Compare commits
1 Commits
priorities
...
styling
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e56c93c3f |
@@ -1,5 +0,0 @@
|
|||||||
**/node_modules/
|
|
||||||
.git
|
|
||||||
**/.venv/
|
|
||||||
**/.env*
|
|
||||||
**/.next/
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
*.schema.json
|
|
||||||
app/pnpm-lock.yaml
|
|
||||||
@@ -32,11 +32,5 @@ NEXT_PUBLIC_HOST="http://localhost:3000"
|
|||||||
GITHUB_CLIENT_ID="your_client_id"
|
GITHUB_CLIENT_ID="your_client_id"
|
||||||
GITHUB_CLIENT_SECRET="your_secret"
|
GITHUB_CLIENT_SECRET="your_secret"
|
||||||
|
|
||||||
OPENPIPE_BASE_URL="http://localhost:3000/api/v1"
|
OPENPIPE_BASE_URL="http://localhost:3000/api"
|
||||||
OPENPIPE_API_KEY="your_key"
|
OPENPIPE_API_KEY="your_key"
|
||||||
|
|
||||||
SENDER_EMAIL="placeholder"
|
|
||||||
SMTP_HOST="placeholder"
|
|
||||||
SMTP_PORT="placeholder"
|
|
||||||
SMTP_LOGIN="placeholder"
|
|
||||||
SMTP_PASSWORD="placeholder"
|
|
||||||
|
|||||||
2
app/.prettierignore
Normal file
2
app/.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*.schema.json
|
||||||
|
pnpm-lock.yaml
|
||||||
9
app/@types/nextjs-routes.d.ts
vendored
9
app/@types/nextjs-routes.d.ts
vendored
@@ -12,19 +12,18 @@ declare module "nextjs-routes" {
|
|||||||
|
|
||||||
export type Route =
|
export type Route =
|
||||||
| StaticRoute<"/account/signin">
|
| StaticRoute<"/account/signin">
|
||||||
| StaticRoute<"/admin/jobs">
|
| DynamicRoute<"/api/[...trpc]", { "trpc": string[] }>
|
||||||
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
|
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
|
||||||
| StaticRoute<"/api/experiments/og-image">
|
| StaticRoute<"/api/experiments/og-image">
|
||||||
|
| StaticRoute<"/api/openapi">
|
||||||
|
| StaticRoute<"/api/sentry-example-api">
|
||||||
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
||||||
| DynamicRoute<"/api/v1/[...trpc]", { "trpc": string[] }>
|
|
||||||
| StaticRoute<"/api/v1/openapi">
|
|
||||||
| StaticRoute<"/dashboard">
|
| StaticRoute<"/dashboard">
|
||||||
| DynamicRoute<"/data/[id]", { "id": string }>
|
| DynamicRoute<"/data/[id]", { "id": string }>
|
||||||
| StaticRoute<"/data">
|
| StaticRoute<"/data">
|
||||||
| DynamicRoute<"/experiments/[experimentSlug]", { "experimentSlug": string }>
|
| DynamicRoute<"/experiments/[id]", { "id": string }>
|
||||||
| StaticRoute<"/experiments">
|
| StaticRoute<"/experiments">
|
||||||
| StaticRoute<"/">
|
| StaticRoute<"/">
|
||||||
| DynamicRoute<"/invitations/[invitationToken]", { "invitationToken": string }>
|
|
||||||
| StaticRoute<"/project/settings">
|
| StaticRoute<"/project/settings">
|
||||||
| StaticRoute<"/request-logs">
|
| StaticRoute<"/request-logs">
|
||||||
| StaticRoute<"/sentry-example-page">
|
| StaticRoute<"/sentry-example-page">
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ RUN yarn global add pnpm
|
|||||||
# DEPS
|
# DEPS
|
||||||
FROM base as deps
|
FROM base as deps
|
||||||
|
|
||||||
WORKDIR /code
|
WORKDIR /app
|
||||||
|
|
||||||
COPY app/prisma app/package.json ./app/
|
COPY prisma ./
|
||||||
COPY client-libs/typescript/package.json ./client-libs/typescript/
|
|
||||||
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
||||||
|
|
||||||
RUN cd app && pnpm install --frozen-lockfile
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# BUILDER
|
# BUILDER
|
||||||
FROM base as builder
|
FROM base as builder
|
||||||
@@ -25,24 +25,22 @@ ARG NEXT_PUBLIC_SENTRY_DSN
|
|||||||
ARG SENTRY_AUTH_TOKEN
|
ARG SENTRY_AUTH_TOKEN
|
||||||
ARG NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS
|
ARG NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS
|
||||||
|
|
||||||
WORKDIR /code
|
WORKDIR /app
|
||||||
COPY --from=deps /code/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY --from=deps /code/app/node_modules ./app/node_modules
|
|
||||||
COPY --from=deps /code/client-libs/typescript/node_modules ./client-libs/typescript/node_modules
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN cd app && SKIP_ENV_VALIDATION=1 pnpm build
|
RUN SKIP_ENV_VALIDATION=1 pnpm build
|
||||||
|
|
||||||
# RUNNER
|
# RUNNER
|
||||||
FROM base as runner
|
FROM base as runner
|
||||||
WORKDIR /code/app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
COPY --from=builder /code/ /code/
|
COPY --from=builder /app/ ./
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
ENV PORT 3000
|
ENV PORT 3000
|
||||||
|
|
||||||
# Run the "run-prod.sh" script
|
# Run the "run-prod.sh" script
|
||||||
CMD /code/app/scripts/run-prod.sh
|
CMD /app/run-prod.sh
|
||||||
@@ -10,15 +10,14 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"dev:next": "TZ=UTC next dev",
|
"dev:next": "next dev",
|
||||||
"dev:wss": "pnpm tsx --watch src/wss-server.ts",
|
"dev:wss": "pnpm tsx --watch src/wss-server.ts",
|
||||||
"worker": "NODE_ENV='development' pnpm tsx --watch src/server/tasks/worker.ts",
|
"dev:worker": "NODE_ENV='development' pnpm tsx --watch src/server/tasks/worker.ts",
|
||||||
"dev": "concurrently --kill-others 'pnpm dev:next' 'pnpm dev:wss' 'pnpm worker --watch'",
|
"dev": "concurrently --kill-others 'pnpm dev:next' 'pnpm dev:wss' 'pnpm dev:worker'",
|
||||||
"postinstall": "prisma generate",
|
"postinstall": "prisma generate",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"start": "TZ=UTC next start",
|
"start": "next start",
|
||||||
"codegen:clients": "tsx src/server/scripts/client-codegen.ts",
|
"codegen:clients": "tsx src/server/scripts/client-codegen.ts",
|
||||||
"codegen:db": "prisma generate && kysely-codegen --dialect postgres --out-file src/server/db.types.ts",
|
|
||||||
"seed": "tsx prisma/seed.ts",
|
"seed": "tsx prisma/seed.ts",
|
||||||
"check": "concurrently 'pnpm lint' 'pnpm tsc' 'pnpm prettier . --check'",
|
"check": "concurrently 'pnpm lint' 'pnpm tsc' 'pnpm prettier . --check'",
|
||||||
"test": "pnpm vitest"
|
"test": "pnpm vitest"
|
||||||
@@ -38,7 +37,6 @@
|
|||||||
"@monaco-editor/loader": "^1.3.3",
|
"@monaco-editor/loader": "^1.3.3",
|
||||||
"@next-auth/prisma-adapter": "^1.0.5",
|
"@next-auth/prisma-adapter": "^1.0.5",
|
||||||
"@prisma/client": "^4.14.0",
|
"@prisma/client": "^4.14.0",
|
||||||
"@sendinblue/client": "^3.3.1",
|
|
||||||
"@sentry/nextjs": "^7.61.0",
|
"@sentry/nextjs": "^7.61.0",
|
||||||
"@t3-oss/env-nextjs": "^0.3.1",
|
"@t3-oss/env-nextjs": "^0.3.1",
|
||||||
"@tabler/icons-react": "^2.22.0",
|
"@tabler/icons-react": "^2.22.0",
|
||||||
@@ -66,18 +64,14 @@
|
|||||||
"json-stringify-pretty-compact": "^4.0.0",
|
"json-stringify-pretty-compact": "^4.0.0",
|
||||||
"jsonschema": "^1.4.1",
|
"jsonschema": "^1.4.1",
|
||||||
"kysely": "^0.26.1",
|
"kysely": "^0.26.1",
|
||||||
"kysely-codegen": "^0.10.1",
|
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-react": "^0.265.0",
|
"lucide-react": "^0.265.0",
|
||||||
"marked": "^7.0.3",
|
|
||||||
"next": "^13.4.2",
|
"next": "^13.4.2",
|
||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"next-query-params": "^4.2.3",
|
"next-query-params": "^4.2.3",
|
||||||
"nextjs-cors": "^2.1.2",
|
"nextjs-cors": "^2.1.2",
|
||||||
"nextjs-routes": "^2.0.1",
|
"nextjs-routes": "^2.0.1",
|
||||||
"nodemailer": "^6.9.4",
|
|
||||||
"openai": "4.0.0-beta.7",
|
"openai": "4.0.0-beta.7",
|
||||||
"openpipe": "workspace:*",
|
|
||||||
"pg": "^8.11.2",
|
"pg": "^8.11.2",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"posthog-js": "^1.75.3",
|
"posthog-js": "^1.75.3",
|
||||||
@@ -106,7 +100,8 @@
|
|||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vite-tsconfig-paths": "^4.2.0",
|
"vite-tsconfig-paths": "^4.2.0",
|
||||||
"zod": "^3.21.4",
|
"zod": "^3.21.4",
|
||||||
"zustand": "^4.3.9"
|
"zustand": "^4.3.9",
|
||||||
|
"openpipe": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@openapi-contrib/openapi-schema-to-json-schema": "^4.0.5",
|
"@openapi-contrib/openapi-schema-to-json-schema": "^4.0.5",
|
||||||
@@ -119,7 +114,6 @@
|
|||||||
"@types/json-schema": "^7.0.12",
|
"@types/json-schema": "^7.0.12",
|
||||||
"@types/lodash-es": "^4.17.8",
|
"@types/lodash-es": "^4.17.8",
|
||||||
"@types/node": "^18.16.0",
|
"@types/node": "^18.16.0",
|
||||||
"@types/nodemailer": "^6.4.9",
|
|
||||||
"@types/pg": "^8.10.2",
|
"@types/pg": "^8.10.2",
|
||||||
"@types/pluralize": "^0.0.30",
|
"@types/pluralize": "^0.0.30",
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
@@ -135,7 +129,6 @@
|
|||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"monaco-editor": "^0.40.0",
|
"monaco-editor": "^0.40.0",
|
||||||
"openapi-typescript": "^6.3.4",
|
"openapi-typescript": "^6.3.4",
|
||||||
"openapi-typescript-codegen": "^0.25.0",
|
|
||||||
"prisma": "^4.14.0",
|
"prisma": "^4.14.0",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
-- DropIndex
|
|
||||||
DROP INDEX "LoggedCallTag_name_idx";
|
|
||||||
DROP INDEX "LoggedCallTag_name_value_idx";
|
|
||||||
|
|
||||||
-- AlterTable: Add projectId column without NOT NULL constraint for now
|
|
||||||
ALTER TABLE "LoggedCallTag" ADD COLUMN "projectId" UUID;
|
|
||||||
|
|
||||||
-- Set the default value
|
|
||||||
UPDATE "LoggedCallTag" lct
|
|
||||||
SET "projectId" = lc."projectId"
|
|
||||||
FROM "LoggedCall" lc
|
|
||||||
WHERE lct."loggedCallId" = lc.id;
|
|
||||||
|
|
||||||
-- Now set the NOT NULL constraint
|
|
||||||
ALTER TABLE "LoggedCallTag" ALTER COLUMN "projectId" SET NOT NULL;
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "LoggedCallTag_projectId_name_idx" ON "LoggedCallTag"("projectId", "name");
|
|
||||||
CREATE INDEX "LoggedCallTag_projectId_name_value_idx" ON "LoggedCallTag"("projectId", "name", "value");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "LoggedCallTag_loggedCallId_name_key" ON "LoggedCallTag"("loggedCallId", "name");
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "UserInvitation" (
|
|
||||||
"id" UUID NOT NULL,
|
|
||||||
"projectId" UUID NOT NULL,
|
|
||||||
"email" TEXT NOT NULL,
|
|
||||||
"role" "ProjectUserRole" NOT NULL,
|
|
||||||
"invitationToken" TEXT NOT NULL,
|
|
||||||
"senderId" UUID NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "UserInvitation_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "UserInvitation_invitationToken_key" ON "UserInvitation"("invitationToken");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "UserInvitation_projectId_email_key" ON "UserInvitation"("projectId", "email");
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "UserInvitation" ADD CONSTRAINT "UserInvitation_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "UserInvitation" ADD CONSTRAINT "UserInvitation_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2023 Viascom Ltd liab. Co
|
|
||||||
*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION nanoid(
|
|
||||||
size int DEFAULT 21,
|
|
||||||
alphabet text DEFAULT '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
||||||
)
|
|
||||||
RETURNS text
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
volatile
|
|
||||||
AS
|
|
||||||
$$
|
|
||||||
DECLARE
|
|
||||||
idBuilder text := '';
|
|
||||||
counter int := 0;
|
|
||||||
bytes bytea;
|
|
||||||
alphabetIndex int;
|
|
||||||
alphabetArray text[];
|
|
||||||
alphabetLength int;
|
|
||||||
mask int;
|
|
||||||
step int;
|
|
||||||
BEGIN
|
|
||||||
alphabetArray := regexp_split_to_array(alphabet, '');
|
|
||||||
alphabetLength := array_length(alphabetArray, 1);
|
|
||||||
mask := (2 << cast(floor(log(alphabetLength - 1) / log(2)) as int)) - 1;
|
|
||||||
step := cast(ceil(1.6 * mask * size / alphabetLength) AS int);
|
|
||||||
|
|
||||||
while true
|
|
||||||
loop
|
|
||||||
bytes := gen_random_bytes(step);
|
|
||||||
while counter < step
|
|
||||||
loop
|
|
||||||
alphabetIndex := (get_byte(bytes, counter) & mask) + 1;
|
|
||||||
if alphabetIndex <= alphabetLength then
|
|
||||||
idBuilder := idBuilder || alphabetArray[alphabetIndex];
|
|
||||||
if length(idBuilder) = size then
|
|
||||||
return idBuilder;
|
|
||||||
end if;
|
|
||||||
end if;
|
|
||||||
counter := counter + 1;
|
|
||||||
end loop;
|
|
||||||
|
|
||||||
counter := 0;
|
|
||||||
end loop;
|
|
||||||
END
|
|
||||||
$$;
|
|
||||||
|
|
||||||
|
|
||||||
-- Make a short_nanoid function that uses the default alphabet and length of 15
|
|
||||||
CREATE OR REPLACE FUNCTION short_nanoid()
|
|
||||||
RETURNS text
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
volatile
|
|
||||||
AS
|
|
||||||
$$
|
|
||||||
BEGIN
|
|
||||||
RETURN nanoid(15, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
|
||||||
END
|
|
||||||
$$;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Experiment" ADD COLUMN "slug" TEXT NOT NULL DEFAULT short_nanoid();
|
|
||||||
|
|
||||||
-- For existing experiments, keep the existing id as the slug for backwards compatibility
|
|
||||||
UPDATE "Experiment" SET "slug" = "id";
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "Experiment_slug_key" ON "Experiment"("slug");
|
|
||||||
@@ -11,9 +11,7 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Experiment {
|
model Experiment {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
slug String @unique @default(dbgenerated("short_nanoid()"))
|
|
||||||
label String
|
label String
|
||||||
|
|
||||||
sortIndex Int @default(0)
|
sortIndex Int @default(0)
|
||||||
@@ -114,17 +112,17 @@ model ScenarioVariantCell {
|
|||||||
model ModelResponse {
|
model ModelResponse {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
cacheKey String
|
cacheKey String
|
||||||
requestedAt DateTime?
|
requestedAt DateTime?
|
||||||
receivedAt DateTime?
|
receivedAt DateTime?
|
||||||
respPayload Json?
|
respPayload Json?
|
||||||
cost Float?
|
cost Float?
|
||||||
inputTokens Int?
|
inputTokens Int?
|
||||||
outputTokens Int?
|
outputTokens Int?
|
||||||
statusCode Int?
|
statusCode Int?
|
||||||
errorMessage String?
|
errorMessage String?
|
||||||
retryTime DateTime?
|
retryTime DateTime?
|
||||||
outdated Boolean @default(false)
|
outdated Boolean @default(false)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -209,14 +207,13 @@ model Project {
|
|||||||
personalProjectUserId String? @unique @db.Uuid
|
personalProjectUserId String? @unique @db.Uuid
|
||||||
personalProjectUser User? @relation(fields: [personalProjectUserId], references: [id], onDelete: Cascade)
|
personalProjectUser User? @relation(fields: [personalProjectUserId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
projectUsers ProjectUser[]
|
projectUsers ProjectUser[]
|
||||||
projectUserInvitations UserInvitation[]
|
experiments Experiment[]
|
||||||
experiments Experiment[]
|
datasets Dataset[]
|
||||||
datasets Dataset[]
|
loggedCalls LoggedCall[]
|
||||||
loggedCalls LoggedCall[]
|
apiKeys ApiKey[]
|
||||||
apiKeys ApiKey[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ProjectUserRole {
|
enum ProjectUserRole {
|
||||||
@@ -276,8 +273,8 @@ model LoggedCall {
|
|||||||
projectId String @db.Uuid
|
projectId String @db.Uuid
|
||||||
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
model String?
|
model String?
|
||||||
tags LoggedCallTag[]
|
tags LoggedCallTag[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -298,7 +295,7 @@ model LoggedCallModelResponse {
|
|||||||
errorMessage String?
|
errorMessage String?
|
||||||
|
|
||||||
requestedAt DateTime
|
requestedAt DateTime
|
||||||
receivedAt DateTime
|
receivedAt DateTime
|
||||||
|
|
||||||
// Note: the function to calculate the cacheKey should include the project
|
// 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
|
// ID so we don't share cached responses between projects, which could be an
|
||||||
@@ -326,17 +323,15 @@ model LoggedCallModelResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model LoggedCallTag {
|
model LoggedCallTag {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
name String
|
name String
|
||||||
value String?
|
value String?
|
||||||
projectId String @db.Uuid
|
|
||||||
|
|
||||||
loggedCallId String @db.Uuid
|
loggedCallId String @db.Uuid
|
||||||
loggedCall LoggedCall @relation(fields: [loggedCallId], references: [id], onDelete: Cascade)
|
loggedCall LoggedCall @relation(fields: [loggedCallId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([loggedCallId, name])
|
@@index([name])
|
||||||
@@index([projectId, name])
|
@@index([name, value])
|
||||||
@@index([projectId, name, value])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model ApiKey {
|
model ApiKey {
|
||||||
@@ -345,8 +340,8 @@ model ApiKey {
|
|||||||
name String
|
name String
|
||||||
apiKey String @unique
|
apiKey String @unique
|
||||||
|
|
||||||
projectId String @db.Uuid
|
projectId String @db.Uuid
|
||||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -393,33 +388,16 @@ model User {
|
|||||||
|
|
||||||
role UserRole @default(USER)
|
role UserRole @default(USER)
|
||||||
|
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
projectUsers ProjectUser[]
|
projectUsers ProjectUser[]
|
||||||
projects Project[]
|
projects Project[]
|
||||||
worldChampEntrant WorldChampEntrant?
|
worldChampEntrant WorldChampEntrant?
|
||||||
sentUserInvitations UserInvitation[]
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model UserInvitation {
|
|
||||||
id String @id @default(uuid()) @db.Uuid
|
|
||||||
|
|
||||||
projectId String @db.Uuid
|
|
||||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
||||||
email String
|
|
||||||
role ProjectUserRole
|
|
||||||
invitationToken String @unique
|
|
||||||
senderId String @db.Uuid
|
|
||||||
sender User @relation(fields: [senderId], references: [id], onDelete: Cascade)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@unique([projectId, email])
|
|
||||||
}
|
|
||||||
|
|
||||||
model VerificationToken {
|
model VerificationToken {
|
||||||
identifier String
|
identifier String
|
||||||
token String @unique
|
token String @unique
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { prisma } from "~/server/db";
|
|||||||
import dedent from "dedent";
|
import dedent from "dedent";
|
||||||
import { generateNewCell } from "~/server/utils/generateNewCell";
|
import { generateNewCell } from "~/server/utils/generateNewCell";
|
||||||
import { promptConstructorVersion } from "~/promptConstructor/version";
|
import { promptConstructorVersion } from "~/promptConstructor/version";
|
||||||
import { env } from "~/env.mjs";
|
|
||||||
|
|
||||||
const defaultId = "11111111-1111-1111-1111-111111111111";
|
const defaultId = "11111111-1111-1111-1111-111111111111";
|
||||||
|
|
||||||
@@ -10,14 +9,6 @@ await prisma.project.deleteMany({
|
|||||||
where: { id: defaultId },
|
where: { id: defaultId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mark all users as admins
|
|
||||||
await prisma.user.updateMany({
|
|
||||||
where: {},
|
|
||||||
data: {
|
|
||||||
role: "ADMIN",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// If there's an existing project, just seed into it
|
// If there's an existing project, just seed into it
|
||||||
const project =
|
const project =
|
||||||
(await prisma.project.findFirst({})) ??
|
(await prisma.project.findFirst({})) ??
|
||||||
@@ -25,20 +16,6 @@ const project =
|
|||||||
data: { id: defaultId },
|
data: { id: defaultId },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (env.OPENPIPE_API_KEY) {
|
|
||||||
await prisma.apiKey.upsert({
|
|
||||||
where: {
|
|
||||||
apiKey: env.OPENPIPE_API_KEY,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
projectId: project.id,
|
|
||||||
name: "Default API Key",
|
|
||||||
apiKey: env.OPENPIPE_API_KEY,
|
|
||||||
},
|
|
||||||
update: {},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.experiment.deleteMany({
|
await prisma.experiment.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: defaultId,
|
id: defaultId,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: number;
|
inputTokens: number;
|
||||||
outputTokens: number;
|
outputTokens: number;
|
||||||
finishReason: string;
|
finishReason: string;
|
||||||
tags: { name: string; value: string }[];
|
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
reqPayload: {
|
reqPayload: {
|
||||||
@@ -108,7 +107,6 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: 236,
|
inputTokens: 236,
|
||||||
outputTokens: 5,
|
outputTokens: 5,
|
||||||
finishReason: "stop",
|
finishReason: "stop",
|
||||||
tags: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
reqPayload: {
|
reqPayload: {
|
||||||
@@ -195,7 +193,6 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: 222,
|
inputTokens: 222,
|
||||||
outputTokens: 5,
|
outputTokens: 5,
|
||||||
finishReason: "stop",
|
finishReason: "stop",
|
||||||
tags: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
reqPayload: {
|
reqPayload: {
|
||||||
@@ -234,7 +231,6 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: 14,
|
inputTokens: 14,
|
||||||
outputTokens: 7,
|
outputTokens: 7,
|
||||||
finishReason: "stop",
|
finishReason: "stop",
|
||||||
tags: [{ name: "prompt_id", value: "id2" }],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
reqPayload: {
|
reqPayload: {
|
||||||
@@ -310,10 +306,6 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: 2802,
|
inputTokens: 2802,
|
||||||
outputTokens: 108,
|
outputTokens: 108,
|
||||||
finishReason: "stop",
|
finishReason: "stop",
|
||||||
tags: [
|
|
||||||
{ name: "prompt_id", value: "chatcmpl-7lQS3MktOT8BTgNEytl9dkyssCQqL" },
|
|
||||||
{ name: "some_other_tag", value: "some_other_value" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -357,7 +349,6 @@ for (let i = 0; i < 1437; i++) {
|
|||||||
cacheHit: false,
|
cacheHit: false,
|
||||||
requestedAt,
|
requestedAt,
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
model: template.reqPayload.model,
|
|
||||||
createdAt: requestedAt,
|
createdAt: requestedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -397,14 +388,11 @@ for (let i = 0; i < 1437; i++) {
|
|||||||
modelResponseId: loggedCallModelResponseId,
|
modelResponseId: loggedCallModelResponseId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
for (const tag of template.tags) {
|
loggedCallTagsToCreate.push({
|
||||||
loggedCallTagsToCreate.push({
|
loggedCallId,
|
||||||
projectId: project.id,
|
name: "$model",
|
||||||
loggedCallId,
|
value: template.reqPayload.model,
|
||||||
name: tag.name,
|
});
|
||||||
value: tag.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.$transaction([
|
await prisma.$transaction([
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ pnpm tsx src/promptConstructor/migrate.ts
|
|||||||
|
|
||||||
echo "Starting the server"
|
echo "Starting the server"
|
||||||
|
|
||||||
pnpm start
|
pnpm concurrently --kill-others \
|
||||||
|
"pnpm start" \
|
||||||
|
"pnpm tsx src/server/tasks/worker.ts"
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#! /bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y htop psql
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#! /bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "Migrating the database"
|
|
||||||
pnpm prisma migrate deploy
|
|
||||||
|
|
||||||
echo "Starting 4 workers"
|
|
||||||
|
|
||||||
pnpm concurrently "pnpm worker" "pnpm worker" "pnpm worker" "pnpm worker"
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#! /bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cd "$(dirname "$0")/../.."
|
|
||||||
|
|
||||||
echo "Env is"
|
|
||||||
echo $ENVIRONMENT
|
|
||||||
|
|
||||||
docker build . --file app/Dockerfile --tag "openpipe-prod"
|
|
||||||
|
|
||||||
# Run the image
|
|
||||||
docker run --env-file app/.env -it --entrypoint "/bin/bash" "openpipe-prod"
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Textarea, type TextareaProps } from "@chakra-ui/react";
|
import { Textarea, type TextareaProps } from "@chakra-ui/react";
|
||||||
import ResizeTextarea from "react-textarea-autosize";
|
import ResizeTextarea from "react-textarea-autosize";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useLayoutEffect, useState } from "react";
|
||||||
|
|
||||||
export const AutoResizeTextarea: React.ForwardRefRenderFunction<
|
export const AutoResizeTextarea: React.ForwardRefRenderFunction<
|
||||||
HTMLTextAreaElement,
|
HTMLTextAreaElement,
|
||||||
TextareaProps & { minRows?: number }
|
TextareaProps & { minRows?: number }
|
||||||
> = ({ minRows = 1, overflowY = "hidden", ...props }, ref) => {
|
> = ({ minRows = 1, overflowY = "hidden", ...props }, ref) => {
|
||||||
const [isRerendered, setIsRerendered] = useState(false);
|
const [isRerendered, setIsRerendered] = useState(false);
|
||||||
useEffect(() => setIsRerendered(true), []);
|
useLayoutEffect(() => setIsRerendered(true), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const ModelStatsCard = ({
|
|||||||
label="Price"
|
label="Price"
|
||||||
info={
|
info={
|
||||||
<Text>
|
<Text>
|
||||||
${model.pricePerSecond.toFixed(4)}
|
${model.pricePerSecond.toFixed(3)}
|
||||||
<Text color="gray.500"> / second</Text>
|
<Text color="gray.500"> / second</Text>
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { HStack, Icon, IconButton, Tooltip, Text, type StackProps } from "@chakra-ui/react";
|
import { HStack, Icon, IconButton, Tooltip, Text } from "@chakra-ui/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { MdContentCopy } from "react-icons/md";
|
import { MdContentCopy } from "react-icons/md";
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
|
||||||
const CopiableCode = ({ code, ...rest }: { code: string } & StackProps) => {
|
const CopiableCode = ({ code }: { code: string }) => {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const [copyToClipboard] = useHandledAsyncCallback(async () => {
|
const [copyToClipboard] = useHandledAsyncCallback(async () => {
|
||||||
await navigator.clipboard.writeText(code);
|
await navigator.clipboard.writeText(code);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
}, [code]);
|
}, [code]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
backgroundColor="blackAlpha.800"
|
backgroundColor="blackAlpha.800"
|
||||||
@@ -19,19 +18,9 @@ const CopiableCode = ({ code, ...rest }: { code: string } & StackProps) => {
|
|||||||
padding={3}
|
padding={3}
|
||||||
w="full"
|
w="full"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="flex-start"
|
|
||||||
{...rest}
|
|
||||||
>
|
>
|
||||||
<Text
|
<Text fontFamily="inconsolata" fontWeight="bold" letterSpacing={0.5} overflowX="auto">
|
||||||
fontFamily="inconsolata"
|
|
||||||
fontWeight="bold"
|
|
||||||
letterSpacing={0.5}
|
|
||||||
overflowX="auto"
|
|
||||||
whiteSpace="pre-wrap"
|
|
||||||
>
|
|
||||||
{code}
|
{code}
|
||||||
{/* Necessary for trailing newline to actually be displayed */}
|
|
||||||
{code.endsWith("\n") ? "\n" : ""}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip closeOnClick={false} label={copied ? "Copied!" : "Copy to clipboard"}>
|
<Tooltip closeOnClick={false} label={copied ? "Copied!" : "Copy to clipboard"}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import {
|
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
InputRightElement,
|
|
||||||
Icon,
|
|
||||||
Popover,
|
|
||||||
PopoverTrigger,
|
|
||||||
PopoverContent,
|
|
||||||
VStack,
|
|
||||||
HStack,
|
|
||||||
Button,
|
|
||||||
Text,
|
|
||||||
useDisclosure,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
|
|
||||||
import { FiChevronDown } from "react-icons/fi";
|
|
||||||
import { BiCheck } from "react-icons/bi";
|
|
||||||
|
|
||||||
type InputDropdownProps<T> = {
|
|
||||||
options: ReadonlyArray<T>;
|
|
||||||
selectedOption: T;
|
|
||||||
onSelect: (option: T) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const InputDropdown = <T,>({ options, selectedOption, onSelect }: InputDropdownProps<T>) => {
|
|
||||||
const popover = useDisclosure();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover placement="bottom-start" {...popover}>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<InputGroup cursor="pointer" w={(selectedOption as string).length * 14 + 180}>
|
|
||||||
<Input
|
|
||||||
value={selectedOption as string}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function -- controlled input requires onChange
|
|
||||||
onChange={() => {}}
|
|
||||||
cursor="pointer"
|
|
||||||
borderColor={popover.isOpen ? "blue.500" : undefined}
|
|
||||||
_hover={popover.isOpen ? { borderColor: "blue.500" } : undefined}
|
|
||||||
contentEditable={false}
|
|
||||||
// disable focus
|
|
||||||
onFocus={(e) => {
|
|
||||||
e.target.blur();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<InputRightElement>
|
|
||||||
<Icon as={FiChevronDown} />
|
|
||||||
</InputRightElement>
|
|
||||||
</InputGroup>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);" minW={0} w="auto">
|
|
||||||
<VStack spacing={0}>
|
|
||||||
{options?.map((option, index) => (
|
|
||||||
<HStack
|
|
||||||
key={index}
|
|
||||||
as={Button}
|
|
||||||
onClick={() => {
|
|
||||||
onSelect(option);
|
|
||||||
popover.onClose();
|
|
||||||
}}
|
|
||||||
w="full"
|
|
||||||
variant="ghost"
|
|
||||||
justifyContent="space-between"
|
|
||||||
fontWeight="semibold"
|
|
||||||
borderRadius={0}
|
|
||||||
colorScheme="blue"
|
|
||||||
color="black"
|
|
||||||
fontSize="sm"
|
|
||||||
borderBottomWidth={1}
|
|
||||||
>
|
|
||||||
<Text mr={16}>{option as string}</Text>
|
|
||||||
{option === selectedOption && <Icon as={BiCheck} color="blue.500" boxSize={5} />}
|
|
||||||
</HStack>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InputDropdown;
|
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
useHandledAsyncCallback,
|
useHandledAsyncCallback,
|
||||||
useVisibleScenarioIds,
|
useVisibleScenarioIds,
|
||||||
} from "~/utils/hooks";
|
} from "~/utils/hooks";
|
||||||
import { cellPadding } from "./constants";
|
import { cellPadding } from "../constants";
|
||||||
import { ActionButton } from "./ScenariosHeader";
|
import { ActionButton } from "./ScenariosHeader";
|
||||||
|
|
||||||
export default function AddVariantButton() {
|
export default function AddVariantButton() {
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { type StackProps, VStack } from "@chakra-ui/react";
|
|
||||||
import { type RouterOutputs } from "~/utils/api";
|
|
||||||
import { type Scenario } from "../types";
|
|
||||||
import { CellOptions } from "./CellOptions";
|
|
||||||
import { OutputStats } from "./OutputStats";
|
|
||||||
|
|
||||||
const CellWrapper: React.FC<
|
|
||||||
StackProps & {
|
|
||||||
cell: RouterOutputs["scenarioVariantCells"]["get"] | undefined;
|
|
||||||
hardRefetching: boolean;
|
|
||||||
hardRefetch: () => void;
|
|
||||||
mostRecentResponse:
|
|
||||||
| NonNullable<RouterOutputs["scenarioVariantCells"]["get"]>["modelResponses"][0]
|
|
||||||
| undefined;
|
|
||||||
scenario: Scenario;
|
|
||||||
}
|
|
||||||
> = ({ children, cell, hardRefetching, hardRefetch, mostRecentResponse, scenario, ...props }) => (
|
|
||||||
<VStack w="full" alignItems="flex-start" {...props} px={2} py={2} h="100%">
|
|
||||||
{cell && (
|
|
||||||
<CellOptions refetchingOutput={hardRefetching} refetchOutput={hardRefetch} cell={cell} />
|
|
||||||
)}
|
|
||||||
<VStack w="full" alignItems="flex-start" maxH={500} overflowY="auto" flex={1}>
|
|
||||||
{children}
|
|
||||||
</VStack>
|
|
||||||
{mostRecentResponse && <OutputStats modelResponse={mostRecentResponse} scenario={scenario} />}
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default CellWrapper;
|
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
import { Text } from "@chakra-ui/react";
|
import { api } from "~/utils/api";
|
||||||
import stringify from "json-stringify-pretty-compact";
|
import { type PromptVariant, type Scenario } from "../types";
|
||||||
import { Fragment, useEffect, useState, type ReactElement } from "react";
|
import { type StackProps, Text, VStack } from "@chakra-ui/react";
|
||||||
|
import { useScenarioVars, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||||
import { docco } from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
import { docco } from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
||||||
import frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
import stringify from "json-stringify-pretty-compact";
|
||||||
import { api } from "~/utils/api";
|
import { type ReactElement, useState, useEffect, Fragment, useCallback } from "react";
|
||||||
import { useHandledAsyncCallback, useScenarioVars } from "~/utils/hooks";
|
|
||||||
import useSocket from "~/utils/useSocket";
|
import useSocket from "~/utils/useSocket";
|
||||||
import { type PromptVariant, type Scenario } from "../types";
|
import { OutputStats } from "./OutputStats";
|
||||||
import CellWrapper from "./CellWrapper";
|
|
||||||
import { ResponseLog } from "./ResponseLog";
|
|
||||||
import { RetryCountdown } from "./RetryCountdown";
|
import { RetryCountdown } from "./RetryCountdown";
|
||||||
|
import frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
||||||
|
import { ResponseLog } from "./ResponseLog";
|
||||||
|
import { CellOptions } from "./TopActions";
|
||||||
|
|
||||||
const WAITING_MESSAGE_INTERVAL = 20000;
|
const WAITING_MESSAGE_INTERVAL = 20000;
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
if (!templateHasVariables) disabledReason = "Add a value to the scenario variables to see output";
|
if (!templateHasVariables) disabledReason = "Add a value to the scenario variables to see output";
|
||||||
|
|
||||||
const [refetchInterval, setRefetchInterval] = useState<number | false>(false);
|
const [refetchInterval, setRefetchInterval] = useState(0);
|
||||||
const { data: cell, isLoading: queryLoading } = api.scenarioVariantCells.get.useQuery(
|
const { data: cell, isLoading: queryLoading } = api.scenarioVariantCells.get.useQuery(
|
||||||
{ scenarioId: scenario.id, variantId: variant.id },
|
{ scenarioId: scenario.id, variantId: variant.id },
|
||||||
{ refetchInterval },
|
{ refetchInterval },
|
||||||
@@ -43,7 +44,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
type OutputSchema = Parameters<typeof provider.normalizeOutput>[0];
|
type OutputSchema = Parameters<typeof provider.normalizeOutput>[0];
|
||||||
|
|
||||||
const { mutateAsync: hardRefetchMutate } = api.scenarioVariantCells.hardRefetch.useMutation();
|
const { mutateAsync: hardRefetchMutate } = api.scenarioVariantCells.forceRefetch.useMutation();
|
||||||
const [hardRefetch, hardRefetching] = useHandledAsyncCallback(async () => {
|
const [hardRefetch, hardRefetching] = useHandledAsyncCallback(async () => {
|
||||||
await hardRefetchMutate({ scenarioId: scenario.id, variantId: variant.id });
|
await hardRefetchMutate({ scenarioId: scenario.id, variantId: variant.id });
|
||||||
await utils.scenarioVariantCells.get.invalidate({
|
await utils.scenarioVariantCells.get.invalidate({
|
||||||
@@ -63,34 +64,42 @@ export default function OutputCell({
|
|||||||
cell.retrievalStatus === "PENDING" ||
|
cell.retrievalStatus === "PENDING" ||
|
||||||
cell.retrievalStatus === "IN_PROGRESS" ||
|
cell.retrievalStatus === "IN_PROGRESS" ||
|
||||||
hardRefetching;
|
hardRefetching;
|
||||||
|
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : 0), [awaitingOutput]);
|
||||||
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : false), [awaitingOutput]);
|
|
||||||
|
|
||||||
// TODO: disconnect from socket if we're not streaming anymore
|
// TODO: disconnect from socket if we're not streaming anymore
|
||||||
const streamedMessage = useSocket<OutputSchema>(cell?.id);
|
const streamedMessage = useSocket<OutputSchema>(cell?.id);
|
||||||
|
|
||||||
const mostRecentResponse = cell?.modelResponses[cell.modelResponses.length - 1];
|
const mostRecentResponse = cell?.modelResponses[cell.modelResponses.length - 1];
|
||||||
|
|
||||||
const wrapperProps: Parameters<typeof CellWrapper>[0] = {
|
const CellWrapper = useCallback(
|
||||||
cell,
|
({ children, ...props }: StackProps) => (
|
||||||
hardRefetching,
|
<VStack w="full" alignItems="flex-start" {...props} px={2} py={2} h="100%">
|
||||||
hardRefetch,
|
{cell && (
|
||||||
mostRecentResponse,
|
<CellOptions refetchingOutput={hardRefetching} refetchOutput={hardRefetch} cell={cell} />
|
||||||
scenario,
|
)}
|
||||||
};
|
<VStack w="full" alignItems="flex-start" maxH={500} overflowY="auto" flex={1}>
|
||||||
|
{children}
|
||||||
|
</VStack>
|
||||||
|
{mostRecentResponse && (
|
||||||
|
<OutputStats modelResponse={mostRecentResponse} scenario={scenario} />
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
),
|
||||||
|
[hardRefetching, hardRefetch, mostRecentResponse, scenario, cell],
|
||||||
|
);
|
||||||
|
|
||||||
if (!vars) return null;
|
if (!vars) return null;
|
||||||
|
|
||||||
if (!cell && !fetchingOutput)
|
if (!cell && !fetchingOutput)
|
||||||
return (
|
return (
|
||||||
<CellWrapper {...wrapperProps}>
|
<CellWrapper>
|
||||||
<Text color="gray.500">Error retrieving output</Text>
|
<Text color="gray.500">Error retrieving output</Text>
|
||||||
</CellWrapper>
|
</CellWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cell && cell.errorMessage) {
|
if (cell && cell.errorMessage) {
|
||||||
return (
|
return (
|
||||||
<CellWrapper {...wrapperProps}>
|
<CellWrapper>
|
||||||
<Text color="red.500">{cell.errorMessage}</Text>
|
<Text color="red.500">{cell.errorMessage}</Text>
|
||||||
</CellWrapper>
|
</CellWrapper>
|
||||||
);
|
);
|
||||||
@@ -102,12 +111,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
if (showLogs)
|
if (showLogs)
|
||||||
return (
|
return (
|
||||||
<CellWrapper
|
<CellWrapper alignItems="flex-start" fontFamily="inconsolata, monospace" spacing={0}>
|
||||||
{...wrapperProps}
|
|
||||||
alignItems="flex-start"
|
|
||||||
fontFamily="inconsolata, monospace"
|
|
||||||
spacing={0}
|
|
||||||
>
|
|
||||||
{cell?.jobQueuedAt && <ResponseLog time={cell.jobQueuedAt} title="Job queued" />}
|
{cell?.jobQueuedAt && <ResponseLog time={cell.jobQueuedAt} title="Job queued" />}
|
||||||
{cell?.jobStartedAt && <ResponseLog time={cell.jobStartedAt} title="Job started" />}
|
{cell?.jobStartedAt && <ResponseLog time={cell.jobStartedAt} title="Job started" />}
|
||||||
{cell?.modelResponses?.map((response) => {
|
{cell?.modelResponses?.map((response) => {
|
||||||
@@ -116,13 +120,8 @@ export default function OutputCell({
|
|||||||
? response.receivedAt.getTime()
|
? response.receivedAt.getTime()
|
||||||
: Date.now();
|
: Date.now();
|
||||||
if (response.requestedAt) {
|
if (response.requestedAt) {
|
||||||
numWaitingMessages = Math.min(
|
numWaitingMessages = Math.floor(
|
||||||
Math.floor(
|
(relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL,
|
||||||
(relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL,
|
|
||||||
),
|
|
||||||
// Don't try to render more than 15, it'll use too much CPU and
|
|
||||||
// break the page
|
|
||||||
15,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -169,7 +168,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
if (mostRecentResponse?.respPayload && normalizedOutput?.type === "json") {
|
if (mostRecentResponse?.respPayload && normalizedOutput?.type === "json") {
|
||||||
return (
|
return (
|
||||||
<CellWrapper {...wrapperProps}>
|
<CellWrapper>
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
customStyle={{ overflowX: "unset", width: "100%", flex: 1 }}
|
customStyle={{ overflowX: "unset", width: "100%", flex: 1 }}
|
||||||
language="json"
|
language="json"
|
||||||
@@ -188,7 +187,7 @@ export default function OutputCell({
|
|||||||
const contentToDisplay = (normalizedOutput?.type === "text" && normalizedOutput.value) || "";
|
const contentToDisplay = (normalizedOutput?.type === "text" && normalizedOutput.value) || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellWrapper {...wrapperProps}>
|
<CellWrapper>
|
||||||
<Text whiteSpace="pre-wrap">{contentToDisplay}</Text>
|
<Text whiteSpace="pre-wrap">{contentToDisplay}</Text>
|
||||||
</CellWrapper>
|
</CellWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,103 +5,30 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
VStack,
|
|
||||||
Text,
|
|
||||||
Box,
|
|
||||||
type UseDisclosureReturn,
|
type UseDisclosureReturn,
|
||||||
Link,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { api, type RouterOutputs } from "~/utils/api";
|
import { type RouterOutputs } from "~/utils/api";
|
||||||
import { JSONTree } from "react-json-tree";
|
import { JSONTree } from "react-json-tree";
|
||||||
import CopiableCode from "~/components/CopiableCode";
|
|
||||||
|
|
||||||
const theme = {
|
export default function ExpandedModal(props: {
|
||||||
scheme: "chalk",
|
|
||||||
author: "chris kempson (http://chriskempson.com)",
|
|
||||||
base00: "transparent",
|
|
||||||
base01: "#202020",
|
|
||||||
base02: "#303030",
|
|
||||||
base03: "#505050",
|
|
||||||
base04: "#b0b0b0",
|
|
||||||
base05: "#d0d0d0",
|
|
||||||
base06: "#e0e0e0",
|
|
||||||
base07: "#f5f5f5",
|
|
||||||
base08: "#fb9fb1",
|
|
||||||
base09: "#eda987",
|
|
||||||
base0A: "#ddb26f",
|
|
||||||
base0B: "#acc267",
|
|
||||||
base0C: "#12cfc0",
|
|
||||||
base0D: "#6fc2ef",
|
|
||||||
base0E: "#e1a3ee",
|
|
||||||
base0F: "#deaf8f",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PromptModal(props: {
|
|
||||||
cell: NonNullable<RouterOutputs["scenarioVariantCells"]["get"]>;
|
cell: NonNullable<RouterOutputs["scenarioVariantCells"]["get"]>;
|
||||||
disclosure: UseDisclosureReturn;
|
disclosure: UseDisclosureReturn;
|
||||||
}) {
|
}) {
|
||||||
const { data } = api.scenarioVariantCells.getTemplatedPromptMessage.useQuery(
|
|
||||||
{
|
|
||||||
cellId: props.cell.id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: props.disclosure.isOpen,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={props.disclosure.isOpen} onClose={props.disclosure.onClose} size="xl">
|
<Modal isOpen={props.disclosure.isOpen} onClose={props.disclosure.onClose} size="2xl">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>Prompt Details</ModalHeader>
|
<ModalHeader>Prompt</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<VStack py={4} w="">
|
<JSONTree
|
||||||
<VStack w="full" alignItems="flex-start">
|
data={props.cell.prompt}
|
||||||
<Text fontWeight="bold">Full Prompt</Text>
|
invertTheme={true}
|
||||||
<Box
|
theme="chalk"
|
||||||
w="full"
|
shouldExpandNodeInitially={() => true}
|
||||||
p={4}
|
getItemString={() => ""}
|
||||||
alignItems="flex-start"
|
hideRoot
|
||||||
backgroundColor="blackAlpha.800"
|
/>
|
||||||
borderRadius={4}
|
|
||||||
>
|
|
||||||
<JSONTree
|
|
||||||
data={props.cell.prompt}
|
|
||||||
theme={theme}
|
|
||||||
shouldExpandNodeInitially={() => true}
|
|
||||||
getItemString={() => ""}
|
|
||||||
hideRoot
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</VStack>
|
|
||||||
{data?.templatedPrompt && (
|
|
||||||
<VStack w="full" mt={4} alignItems="flex-start">
|
|
||||||
<Text fontWeight="bold">Templated prompt message:</Text>
|
|
||||||
<CopiableCode
|
|
||||||
w="full"
|
|
||||||
// bgColor="gray.100"
|
|
||||||
p={4}
|
|
||||||
borderWidth={1}
|
|
||||||
whiteSpace="pre-wrap"
|
|
||||||
code={data.templatedPrompt}
|
|
||||||
/>
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
{data?.learnMoreUrl && (
|
|
||||||
<Link
|
|
||||||
href={data.learnMoreUrl}
|
|
||||||
isExternal
|
|
||||||
color="blue.500"
|
|
||||||
fontWeight="bold"
|
|
||||||
fontSize="sm"
|
|
||||||
mt={4}
|
|
||||||
alignSelf="flex-end"
|
|
||||||
>
|
|
||||||
Learn More
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { HStack, Icon, IconButton, Spinner, Tooltip, useDisclosure } from "@chakra-ui/react";
|
import { HStack, Icon, IconButton, Spinner, Tooltip, useDisclosure } from "@chakra-ui/react";
|
||||||
import { BsArrowClockwise, BsInfoCircle } from "react-icons/bs";
|
import { BsArrowClockwise, BsInfoCircle } from "react-icons/bs";
|
||||||
import { useExperimentAccess } from "~/utils/hooks";
|
import { useExperimentAccess } from "~/utils/hooks";
|
||||||
import PromptModal from "./PromptModal";
|
import ExpandedModal from "./PromptModal";
|
||||||
import { type RouterOutputs } from "~/utils/api";
|
import { type RouterOutputs } from "~/utils/api";
|
||||||
|
|
||||||
export const CellOptions = ({
|
export const CellOptions = ({
|
||||||
@@ -32,7 +32,7 @@ export const CellOptions = ({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PromptModal cell={cell} disclosure={modalDisclosure} />
|
<ExpandedModal cell={cell} disclosure={modalDisclosure} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{canModify && (
|
{canModify && (
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
VStack,
|
VStack,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { BsArrowsAngleExpand, BsX } from "react-icons/bs";
|
import { BsArrowsAngleExpand, BsX } from "react-icons/bs";
|
||||||
import { cellPadding } from "./constants";
|
import { cellPadding } from "../constants";
|
||||||
import { FloatingLabelInput } from "./FloatingLabelInput";
|
import { FloatingLabelInput } from "./FloatingLabelInput";
|
||||||
import { ScenarioEditorModal } from "./ScenarioEditorModal";
|
import { ScenarioEditorModal } from "./ScenarioEditorModal";
|
||||||
|
|
||||||
@@ -111,23 +111,25 @@ export default function ScenarioEditor({
|
|||||||
onDrop={onReorder}
|
onDrop={onReorder}
|
||||||
backgroundColor={isDragTarget ? "gray.100" : "transparent"}
|
backgroundColor={isDragTarget ? "gray.100" : "transparent"}
|
||||||
>
|
>
|
||||||
{
|
{variableLabels.length === 0 ? (
|
||||||
|
<Box color="gray.500">
|
||||||
|
{vars.data ? "No scenario variables configured" : "Loading..."}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
<VStack spacing={4} flex={1} py={2}>
|
<VStack spacing={4} flex={1} py={2}>
|
||||||
<HStack justifyContent="space-between" w="100%" align="center" spacing={0}>
|
<HStack justifyContent="space-between" w="100%" align="center" spacing={0}>
|
||||||
<Text flex={1}>Scenario</Text>
|
<Text flex={1}>Scenario</Text>
|
||||||
{variableLabels.length && (
|
<Tooltip label="Expand" hasArrow>
|
||||||
<Tooltip label="Expand" hasArrow>
|
<IconButton
|
||||||
<IconButton
|
aria-label="Expand"
|
||||||
aria-label="Expand"
|
icon={<Icon as={BsArrowsAngleExpand} boxSize={3} />}
|
||||||
icon={<Icon as={BsArrowsAngleExpand} boxSize={3} />}
|
onClick={() => setScenarioEditorModalOpen(true)}
|
||||||
onClick={() => setScenarioEditorModalOpen(true)}
|
size="xs"
|
||||||
size="xs"
|
colorScheme="gray"
|
||||||
colorScheme="gray"
|
color="gray.500"
|
||||||
color="gray.500"
|
variant="ghost"
|
||||||
variant="ghost"
|
/>
|
||||||
/>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{canModify && props.canHide && (
|
{canModify && props.canHide && (
|
||||||
<Tooltip label="Delete" hasArrow>
|
<Tooltip label="Delete" hasArrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -148,38 +150,31 @@ export default function ScenarioEditor({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
{variableLabels.map((key) => {
|
||||||
{variableLabels.length === 0 ? (
|
const value = values[key] ?? "";
|
||||||
<Box color="gray.500">
|
return (
|
||||||
{vars.data ? "No scenario variables configured" : "Loading..."}
|
<FloatingLabelInput
|
||||||
</Box>
|
key={key}
|
||||||
) : (
|
label={key}
|
||||||
variableLabels.map((key) => {
|
isDisabled={!canModify}
|
||||||
const value = values[key] ?? "";
|
style={{ width: "100%" }}
|
||||||
return (
|
maxHeight={32}
|
||||||
<FloatingLabelInput
|
value={value}
|
||||||
key={key}
|
onChange={(e) => {
|
||||||
label={key}
|
setValues((prev) => ({ ...prev, [key]: e.target.value }));
|
||||||
isDisabled={!canModify}
|
}}
|
||||||
style={{ width: "100%" }}
|
onKeyDown={(e) => {
|
||||||
maxHeight={32}
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||||
value={value}
|
e.preventDefault();
|
||||||
onChange={(e) => {
|
e.currentTarget.blur();
|
||||||
setValues((prev) => ({ ...prev, [key]: e.target.value }));
|
onSave();
|
||||||
}}
|
}
|
||||||
onKeyDown={(e) => {
|
}}
|
||||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
onMouseEnter={() => setVariableInputHovered(true)}
|
||||||
e.preventDefault();
|
onMouseLeave={() => setVariableInputHovered(false)}
|
||||||
e.currentTarget.blur();
|
/>
|
||||||
onSave();
|
);
|
||||||
}
|
})}
|
||||||
}}
|
|
||||||
onMouseEnter={() => setVariableInputHovered(true)}
|
|
||||||
onMouseLeave={() => setVariableInputHovered(false)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
{hasChanged && (
|
{hasChanged && (
|
||||||
<HStack justify="right">
|
<HStack justify="right">
|
||||||
<Button
|
<Button
|
||||||
@@ -197,7 +192,7 @@ export default function ScenarioEditor({
|
|||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
{scenarioEditorModalOpen && (
|
{scenarioEditorModalOpen && (
|
||||||
<ScenarioEditorModal
|
<ScenarioEditorModal
|
||||||
|
|||||||
@@ -65,11 +65,11 @@ export const ScenarioEditorModal = ({
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen
|
isOpen
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
size={{ base: "xl", sm: "2xl", md: "3xl", lg: "4xl", xl: "5xl" }}
|
size={{ base: "xl", sm: "2xl", md: "3xl", lg: "5xl", xl: "7xl" }}
|
||||||
>
|
>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent w={1200}>
|
<ModalContent w={1200}>
|
||||||
<ModalHeader>Edit Scenario</ModalHeader>
|
<ModalHeader />
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody maxW="unset">
|
<ModalBody maxW="unset">
|
||||||
<VStack spacing={8}>
|
<VStack spacing={8}>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { cellPadding } from "./constants";
|
import { cellPadding } from "../constants";
|
||||||
import {
|
import {
|
||||||
useExperiment,
|
useExperiment,
|
||||||
useExperimentAccess,
|
useExperimentAccess,
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
setIsChanged(false);
|
setIsChanged(false);
|
||||||
|
|
||||||
await utils.promptVariants.list.invalidate();
|
await utils.promptVariants.list.invalidate();
|
||||||
}, [checkForChanges, replaceVariant.mutateAsync]);
|
}, [checkForChanges]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (monaco) {
|
if (monaco) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { HStack, Icon, Text, useToken } from "@chakra-ui/react";
|
import { HStack, Icon, Text, useToken } from "@chakra-ui/react";
|
||||||
import { type PromptVariant } from "./types";
|
import { type PromptVariant } from "./types";
|
||||||
import { cellPadding } from "./constants";
|
import { cellPadding } from "../constants";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import chroma from "chroma-js";
|
import chroma from "chroma-js";
|
||||||
import { BsCurrencyDollar } from "react-icons/bs";
|
import { BsCurrencyDollar } from "react-icons/bs";
|
||||||
@@ -21,18 +21,14 @@ export default function VariantStats(props: { variant: PromptVariant }) {
|
|||||||
outputTokens: 0,
|
outputTokens: 0,
|
||||||
scenarioCount: 0,
|
scenarioCount: 0,
|
||||||
outputCount: 0,
|
outputCount: 0,
|
||||||
awaitingCompletions: false,
|
|
||||||
awaitingEvals: false,
|
awaitingEvals: false,
|
||||||
},
|
},
|
||||||
refetchInterval,
|
refetchInterval,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Poll every five seconds while we are waiting for LLM retrievals to finish
|
// Poll every two seconds while we are waiting for LLM retrievals to finish
|
||||||
useEffect(
|
useEffect(() => setRefetchInterval(data.awaitingEvals ? 5000 : 0), [data.awaitingEvals]);
|
||||||
() => setRefetchInterval(data.awaitingCompletions || data.awaitingEvals ? 5000 : 0),
|
|
||||||
[data.awaitingCompletions, data.awaitingEvals],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [passColor, neutralColor, failColor] = useToken("colors", [
|
const [passColor, neutralColor, failColor] = useToken("colors", [
|
||||||
"green.500",
|
"green.500",
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import { api } from "~/utils/api";
|
|||||||
import AddVariantButton from "./AddVariantButton";
|
import AddVariantButton from "./AddVariantButton";
|
||||||
import ScenarioRow from "./ScenarioRow";
|
import ScenarioRow from "./ScenarioRow";
|
||||||
import VariantEditor from "./VariantEditor";
|
import VariantEditor from "./VariantEditor";
|
||||||
import VariantHeader from "./VariantHeader/VariantHeader";
|
import VariantHeader from "../VariantHeader/VariantHeader";
|
||||||
import VariantStats from "./VariantStats";
|
import VariantStats from "./VariantStats";
|
||||||
import { ScenariosHeader } from "./ScenariosHeader";
|
import { ScenariosHeader } from "./ScenariosHeader";
|
||||||
import { borders } from "./styles";
|
import { borders } from "./styles";
|
||||||
import { useScenarios } from "~/utils/hooks";
|
import { useScenarios } from "~/utils/hooks";
|
||||||
import ScenarioPaginator from "./ScenarioPaginator";
|
import ScenarioPaginator from "./ScenarioPaginator";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import useScrolledPast from "./useHasScrolledPast";
|
|
||||||
|
|
||||||
export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) {
|
export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) {
|
||||||
const variants = api.promptVariants.list.useQuery(
|
const variants = api.promptVariants.list.useQuery(
|
||||||
@@ -19,7 +18,6 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
);
|
);
|
||||||
|
|
||||||
const scenarios = useScenarios();
|
const scenarios = useScenarios();
|
||||||
const shouldFlattenHeader = useScrolledPast(50);
|
|
||||||
|
|
||||||
if (!variants.data || !scenarios.data) return null;
|
if (!variants.data || !scenarios.data) return null;
|
||||||
|
|
||||||
@@ -65,8 +63,8 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
variant={variant}
|
variant={variant}
|
||||||
canHide={variants.data.length > 1}
|
canHide={variants.data.length > 1}
|
||||||
rowStart={1}
|
rowStart={1}
|
||||||
borderTopLeftRadius={isFirst && !shouldFlattenHeader ? 8 : 0}
|
borderTopLeftRadius={isFirst ? 8 : 0}
|
||||||
borderTopRightRadius={isLast && !shouldFlattenHeader ? 8 : 0}
|
borderTopRightRadius={isLast ? 8 : 0}
|
||||||
{...sharedProps}
|
{...sharedProps}
|
||||||
/>
|
/>
|
||||||
<GridItem rowStart={2} {...sharedProps}>
|
<GridItem rowStart={2} {...sharedProps}>
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
|
|
||||||
const useScrolledPast = (scrollThreshold: number) => {
|
|
||||||
const [hasScrolledPast, setHasScrolledPast] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const container = document.getElementById("output-container");
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
console.warn('Element with id "outputs-container" not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkScroll = () => {
|
|
||||||
const { scrollTop } = container;
|
|
||||||
|
|
||||||
// Check if scrollTop is greater than or equal to scrollThreshold
|
|
||||||
setHasScrolledPast(scrollTop > scrollThreshold);
|
|
||||||
};
|
|
||||||
|
|
||||||
checkScroll();
|
|
||||||
|
|
||||||
container.addEventListener("scroll", checkScroll);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => {
|
|
||||||
container.removeEventListener("scroll", checkScroll);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return hasScrolledPast;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useScrolledPast;
|
|
||||||
@@ -37,8 +37,6 @@ const Paginator = ({
|
|||||||
const goToLastPage = () => setPageParams({ page: lastPage }, "replace");
|
const goToLastPage = () => setPageParams({ page: lastPage }, "replace");
|
||||||
const goToFirstPage = () => setPageParams({ page: 1 }, "replace");
|
const goToFirstPage = () => setPageParams({ page: 1 }, "replace");
|
||||||
|
|
||||||
if (count === 0) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
pt={4}
|
pt={4}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useState, type DragEvent } from "react";
|
import { useState, type DragEvent } from "react";
|
||||||
import { type PromptVariant } from "../types";
|
import { type PromptVariant } from "../OutputsTable/types";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { RiDraggable } from "react-icons/ri";
|
import { RiDraggable } from "react-icons/ri";
|
||||||
import { useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
import { HStack, Icon, Text, GridItem, type GridItemProps } from "@chakra-ui/react"; // Changed here
|
import { HStack, Icon, Text, GridItem, type GridItemProps } from "@chakra-ui/react"; // Changed here
|
||||||
import { cellPadding, headerMinHeight } from "../constants";
|
import { cellPadding, headerMinHeight } from "../constants";
|
||||||
import AutoResizeTextArea from "../../AutoResizeTextArea";
|
import AutoResizeTextArea from "../AutoResizeTextArea";
|
||||||
import VariantHeaderMenuButton from "./VariantHeaderMenuButton";
|
import VariantHeaderMenuButton from "./VariantHeaderMenuButton";
|
||||||
|
|
||||||
export default function VariantHeader(
|
export default function VariantHeader(
|
||||||
@@ -75,7 +75,7 @@ export default function VariantHeader(
|
|||||||
padding={0}
|
padding={0}
|
||||||
sx={{
|
sx={{
|
||||||
position: "sticky",
|
position: "sticky",
|
||||||
top: "0",
|
top: "-2",
|
||||||
// Ensure that the menu always appears above the sticky header of other variants
|
// Ensure that the menu always appears above the sticky header of other variants
|
||||||
zIndex: menuOpen ? "dropdown" : 10,
|
zIndex: menuOpen ? "dropdown" : 10,
|
||||||
}}
|
}}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { type PromptVariant } from "../OutputsTable/types";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useHandledAsyncCallback, useVisibleScenarioIds } from "~/utils/hooks";
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Menu,
|
Menu,
|
||||||
@@ -12,13 +14,10 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { BsFillTrashFill, BsGear, BsStars } from "react-icons/bs";
|
import { BsFillTrashFill, BsGear, BsStars } from "react-icons/bs";
|
||||||
import { FaRegClone } from "react-icons/fa";
|
import { FaRegClone } from "react-icons/fa";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { RefinePromptModal } from "../RefinePromptModal/RefinePromptModal";
|
||||||
import { RiExchangeFundsFill } from "react-icons/ri";
|
import { RiExchangeFundsFill } from "react-icons/ri";
|
||||||
|
import { ChangeModelModal } from "../ChangeModelModal/ChangeModelModal";
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useHandledAsyncCallback, useVisibleScenarioIds } from "~/utils/hooks";
|
|
||||||
import { type PromptVariant } from "../types";
|
|
||||||
import { RefinePromptModal } from "../../RefinePromptModal/RefinePromptModal";
|
|
||||||
import { ChangeModelModal } from "../../ChangeModelModal/ChangeModelModal";
|
|
||||||
|
|
||||||
export default function VariantHeaderMenuButton({
|
export default function VariantHeaderMenuButton({
|
||||||
variant,
|
variant,
|
||||||
@@ -14,11 +14,21 @@ import { formatTimePast } from "~/utils/dayjs";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { BsPlusSquare } from "react-icons/bs";
|
import { BsPlusSquare } from "react-icons/bs";
|
||||||
import { RouterOutputs, api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
|
|
||||||
export const ExperimentCard = ({ exp }: { exp: RouterOutputs["experiments"]["list"][0] }) => {
|
type ExperimentData = {
|
||||||
|
testScenarioCount: number;
|
||||||
|
promptVariantCount: number;
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
sortIndex: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
w="full"
|
w="full"
|
||||||
@@ -35,7 +45,7 @@ export const ExperimentCard = ({ exp }: { exp: RouterOutputs["experiments"]["lis
|
|||||||
as={Link}
|
as={Link}
|
||||||
w="full"
|
w="full"
|
||||||
h="full"
|
h="full"
|
||||||
href={{ pathname: "/experiments/[experimentSlug]", query: { experimentSlug: exp.slug } }}
|
href={{ pathname: "/experiments/[id]", query: { id: exp.id } }}
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
>
|
>
|
||||||
<HStack w="full" color="gray.700" justify="center">
|
<HStack w="full" color="gray.700" justify="center">
|
||||||
@@ -79,8 +89,8 @@ export const NewExperimentCard = () => {
|
|||||||
projectId: selectedProjectId ?? "",
|
projectId: selectedProjectId ?? "",
|
||||||
});
|
});
|
||||||
await router.push({
|
await router.push({
|
||||||
pathname: "/experiments/[experimentSlug]",
|
pathname: "/experiments/[id]",
|
||||||
query: { experimentSlug: newExperiment.slug },
|
query: { id: newExperiment.id },
|
||||||
});
|
});
|
||||||
}, [createMutation, router, selectedProjectId]);
|
}, [createMutation, router, selectedProjectId]);
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,11 @@ export const useOnForkButtonPressed = () => {
|
|||||||
|
|
||||||
const [onFork, isForking] = useHandledAsyncCallback(async () => {
|
const [onFork, isForking] = useHandledAsyncCallback(async () => {
|
||||||
if (!experiment.data?.id || !selectedProjectId) return;
|
if (!experiment.data?.id || !selectedProjectId) return;
|
||||||
const newExperiment = await forkMutation.mutateAsync({
|
const forkedExperimentId = await forkMutation.mutateAsync({
|
||||||
id: experiment.data.id,
|
id: experiment.data.id,
|
||||||
projectId: selectedProjectId,
|
projectId: selectedProjectId,
|
||||||
});
|
});
|
||||||
await router.push({
|
await router.push({ pathname: "/experiments/[id]", query: { id: forkedExperimentId } });
|
||||||
pathname: "/experiments/[experimentSlug]",
|
|
||||||
query: { experimentSlug: newExperiment.slug },
|
|
||||||
});
|
|
||||||
}, [forkMutation, experiment.data?.id, router]);
|
}, [forkMutation, experiment.data?.id, router]);
|
||||||
|
|
||||||
const onForkButtonPressed = useCallback(() => {
|
const onForkButtonPressed = useCallback(() => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
Box,
|
Box,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { BsPlus, BsPersonCircle } from "react-icons/bs";
|
import { BsPlus, BsPersonCircle } from "react-icons/bs";
|
||||||
import { type Project } from "@prisma/client";
|
import { type Project } from "@prisma/client";
|
||||||
@@ -67,13 +67,7 @@ export default function ProjectMenu() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack
|
<VStack w="full" alignItems="flex-start" spacing={0} py={1}>
|
||||||
w="full"
|
|
||||||
alignItems="flex-start"
|
|
||||||
spacing={0}
|
|
||||||
py={1}
|
|
||||||
zIndex={popover.isOpen ? "dropdown" : undefined}
|
|
||||||
>
|
|
||||||
<Popover
|
<Popover
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
isOpen={popover.isOpen}
|
isOpen={popover.isOpen}
|
||||||
@@ -111,8 +105,8 @@ export default function ProjectMenu() {
|
|||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
_focusVisible={{ outline: "unset" }}
|
_focusVisible={{ outline: "unset" }}
|
||||||
w={220}
|
ml={-1}
|
||||||
ml={{ base: 2, md: 0 }}
|
w={224}
|
||||||
boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);"
|
boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);"
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
>
|
>
|
||||||
@@ -182,6 +176,7 @@ const ProjectOption = ({
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
|
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
|
||||||
|
const [gearHovered, setGearHovered] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
@@ -193,8 +188,8 @@ const ProjectOption = ({
|
|||||||
}}
|
}}
|
||||||
w="full"
|
w="full"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
_hover={{ bgColor: "gray.200", textDecoration: "none" }}
|
_hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
|
||||||
bgColor={isActive ? "gray.100" : undefined}
|
color={isActive ? "blue.400" : undefined}
|
||||||
py={2}
|
py={2}
|
||||||
px={4}
|
px={4}
|
||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
Input,
|
|
||||||
FormHelperText,
|
|
||||||
HStack,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalContent,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalOverlay,
|
|
||||||
Spinner,
|
|
||||||
Text,
|
|
||||||
VStack,
|
|
||||||
RadioGroup,
|
|
||||||
Radio,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
|
|
||||||
import { maybeReportError } from "~/utils/errorHandling/maybeReportError";
|
|
||||||
import { type ProjectUserRole } from "@prisma/client";
|
|
||||||
|
|
||||||
export const InviteMemberModal = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
}: {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}) => {
|
|
||||||
const selectedProject = useSelectedProject().data;
|
|
||||||
const utils = api.useContext();
|
|
||||||
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [role, setRole] = useState<ProjectUserRole>("MEMBER");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setEmail("");
|
|
||||||
setRole("MEMBER");
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
const emailIsValid = !email || !email.match(/.+@.+\..+/);
|
|
||||||
|
|
||||||
const inviteMemberMutation = api.users.inviteToProject.useMutation();
|
|
||||||
|
|
||||||
const [inviteMember, isInviting] = useHandledAsyncCallback(async () => {
|
|
||||||
if (!selectedProject?.id || !role) return;
|
|
||||||
const resp = await inviteMemberMutation.mutateAsync({
|
|
||||||
projectId: selectedProject.id,
|
|
||||||
email,
|
|
||||||
role,
|
|
||||||
});
|
|
||||||
if (maybeReportError(resp)) return;
|
|
||||||
await utils.projects.get.invalidate();
|
|
||||||
onClose();
|
|
||||||
}, [inviteMemberMutation, email, role, selectedProject?.id, onClose]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose}>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent w={1200}>
|
|
||||||
<ModalHeader>
|
|
||||||
<HStack>
|
|
||||||
<Text>Invite Member</Text>
|
|
||||||
</HStack>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody>
|
|
||||||
<VStack spacing={8} alignItems="flex-start">
|
|
||||||
<Text>
|
|
||||||
Invite a new member to <b>{selectedProject?.name}</b>.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
value={role}
|
|
||||||
onChange={(e) => setRole(e as ProjectUserRole)}
|
|
||||||
colorScheme="orange"
|
|
||||||
>
|
|
||||||
<VStack w="full" alignItems="flex-start">
|
|
||||||
<Radio value="MEMBER">
|
|
||||||
<Text fontSize="sm">MEMBER</Text>
|
|
||||||
</Radio>
|
|
||||||
<Radio value="ADMIN">
|
|
||||||
<Text fontSize="sm">ADMIN</Text>
|
|
||||||
</Radio>
|
|
||||||
</VStack>
|
|
||||||
</RadioGroup>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>Email</FormLabel>
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey || e.shiftKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.currentTarget.blur();
|
|
||||||
inviteMember();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormHelperText>Enter the email of the person you want to invite.</FormHelperText>
|
|
||||||
</FormControl>
|
|
||||||
</VStack>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter mt={4}>
|
|
||||||
<HStack>
|
|
||||||
<Button colorScheme="gray" onClick={onClose} minW={24}>
|
|
||||||
<Text>Cancel</Text>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorScheme="orange"
|
|
||||||
onClick={inviteMember}
|
|
||||||
minW={24}
|
|
||||||
isDisabled={emailIsValid || isInviting}
|
|
||||||
>
|
|
||||||
{isInviting ? <Spinner boxSize={4} /> : <Text>Send Invitation</Text>}
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
import { useMemo, useState } from "react";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
Thead,
|
|
||||||
Tr,
|
|
||||||
Th,
|
|
||||||
Tbody,
|
|
||||||
Td,
|
|
||||||
IconButton,
|
|
||||||
useDisclosure,
|
|
||||||
Text,
|
|
||||||
Button,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { BsTrash } from "react-icons/bs";
|
|
||||||
import { type User } from "@prisma/client";
|
|
||||||
|
|
||||||
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
|
|
||||||
import { InviteMemberModal } from "./InviteMemberModal";
|
|
||||||
import { RemoveMemberDialog } from "./RemoveMemberDialog";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { maybeReportError } from "~/utils/errorHandling/maybeReportError";
|
|
||||||
|
|
||||||
const MemberTable = () => {
|
|
||||||
const selectedProject = useSelectedProject().data;
|
|
||||||
const session = useSession().data;
|
|
||||||
|
|
||||||
const utils = api.useContext();
|
|
||||||
|
|
||||||
const [memberToRemove, setMemberToRemove] = useState<User | null>(null);
|
|
||||||
const inviteMemberModal = useDisclosure();
|
|
||||||
|
|
||||||
const cancelInvitationMutation = api.users.cancelProjectInvitation.useMutation();
|
|
||||||
|
|
||||||
const [cancelInvitation, isCancelling] = useHandledAsyncCallback(
|
|
||||||
async (invitationToken: string) => {
|
|
||||||
if (!selectedProject?.id) return;
|
|
||||||
const resp = await cancelInvitationMutation.mutateAsync({
|
|
||||||
invitationToken,
|
|
||||||
});
|
|
||||||
if (maybeReportError(resp)) return;
|
|
||||||
await utils.projects.get.invalidate();
|
|
||||||
},
|
|
||||||
[selectedProject?.id, cancelInvitationMutation],
|
|
||||||
);
|
|
||||||
|
|
||||||
const sortedMembers = useMemo(() => {
|
|
||||||
if (!selectedProject?.projectUsers) return [];
|
|
||||||
return selectedProject.projectUsers.sort((a, b) => {
|
|
||||||
if (a.role === b.role) return a.createdAt < b.createdAt ? -1 : 1;
|
|
||||||
// Take advantage of fact that ADMIN is alphabetically before MEMBER
|
|
||||||
return a.role < b.role ? -1 : 1;
|
|
||||||
});
|
|
||||||
}, [selectedProject?.projectUsers]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Table fontSize={{ base: "sm", md: "md" }}>
|
|
||||||
<Thead
|
|
||||||
sx={{
|
|
||||||
th: {
|
|
||||||
base: { px: 0 },
|
|
||||||
md: { px: 6 },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tr>
|
|
||||||
<Th>Name</Th>
|
|
||||||
<Th display={{ base: "none", md: "table-cell" }}>Email</Th>
|
|
||||||
<Th>Role</Th>
|
|
||||||
{selectedProject?.role === "ADMIN" && <Th />}
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody
|
|
||||||
sx={{
|
|
||||||
td: {
|
|
||||||
base: { px: 0 },
|
|
||||||
md: { px: 6 },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedProject &&
|
|
||||||
sortedMembers.map((member) => {
|
|
||||||
return (
|
|
||||||
<Tr key={member.id}>
|
|
||||||
<Td>
|
|
||||||
<Text fontWeight="bold">{member.user.name}</Text>
|
|
||||||
</Td>
|
|
||||||
<Td display={{ base: "none", md: "table-cell" }} h="full">
|
|
||||||
{member.user.email}
|
|
||||||
</Td>
|
|
||||||
<Td fontSize={{ base: "xs", md: "sm" }}>{member.role}</Td>
|
|
||||||
{selectedProject.role === "ADMIN" && (
|
|
||||||
<Td textAlign="end">
|
|
||||||
{member.user.id !== session?.user?.id &&
|
|
||||||
member.user.id !== selectedProject.personalProjectUserId && (
|
|
||||||
<IconButton
|
|
||||||
aria-label="Remove member"
|
|
||||||
colorScheme="red"
|
|
||||||
icon={<BsTrash />}
|
|
||||||
onClick={() => setMemberToRemove(member.user)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
)}
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{selectedProject?.projectUserInvitations?.map((invitation) => {
|
|
||||||
return (
|
|
||||||
<Tr key={invitation.id}>
|
|
||||||
<Td>
|
|
||||||
<Text as="i">Invitation pending</Text>
|
|
||||||
</Td>
|
|
||||||
<Td>{invitation.email}</Td>
|
|
||||||
<Td fontSize="sm">{invitation.role}</Td>
|
|
||||||
{selectedProject.role === "ADMIN" && (
|
|
||||||
<Td textAlign="end">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorScheme="red"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => cancelInvitation(invitation.invitationToken)}
|
|
||||||
isLoading={isCancelling}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</Td>
|
|
||||||
)}
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
<InviteMemberModal isOpen={inviteMemberModal.isOpen} onClose={inviteMemberModal.onClose} />
|
|
||||||
<RemoveMemberDialog
|
|
||||||
member={memberToRemove}
|
|
||||||
isOpen={!!memberToRemove}
|
|
||||||
onClose={() => setMemberToRemove(null)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MemberTable;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogBody,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
Text,
|
|
||||||
VStack,
|
|
||||||
Spinner,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { type User } from "@prisma/client";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useRef } from "react";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
|
|
||||||
|
|
||||||
export const RemoveMemberDialog = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
member,
|
|
||||||
}: {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
member: User | null;
|
|
||||||
}) => {
|
|
||||||
const selectedProject = useSelectedProject();
|
|
||||||
const removeUserMutation = api.users.removeUserFromProject.useMutation();
|
|
||||||
const utils = api.useContext();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
const [onRemoveConfirm, isRemoving] = useHandledAsyncCallback(async () => {
|
|
||||||
if (!selectedProject.data?.id || !member?.id) return;
|
|
||||||
await removeUserMutation.mutateAsync({ projectId: selectedProject.data.id, userId: member.id });
|
|
||||||
await utils.projects.get.invalidate();
|
|
||||||
onClose();
|
|
||||||
}, [removeUserMutation, selectedProject, router]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
|
||||||
<AlertDialogOverlay>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
|
||||||
Remove Member
|
|
||||||
</AlertDialogHeader>
|
|
||||||
|
|
||||||
<AlertDialogBody>
|
|
||||||
<VStack spacing={4} alignItems="flex-start">
|
|
||||||
<Text>
|
|
||||||
Are you sure you want to remove <b>{member?.name}</b> from the project?
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</AlertDialogBody>
|
|
||||||
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<Button ref={cancelRef} onClick={onClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button colorScheme="red" onClick={onRemoveConfirm} ml={3} w={20}>
|
|
||||||
{isRemoving ? <Spinner /> : "Remove"}
|
|
||||||
</Button>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialogOverlay>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Button, HStack, Icon, Text } from "@chakra-ui/react";
|
|
||||||
import { BsPlus } from "react-icons/bs";
|
|
||||||
import { comparators, defaultFilterableFields } from "~/state/logFiltersSlice";
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
|
|
||||||
const AddFilterButton = () => {
|
|
||||||
const addFilter = useAppStore((s) => s.logFilters.addFilter);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HStack
|
|
||||||
as={Button}
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() =>
|
|
||||||
addFilter({
|
|
||||||
id: Date.now().toString(),
|
|
||||||
field: defaultFilterableFields[0],
|
|
||||||
comparator: comparators[0],
|
|
||||||
value: "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
spacing={0}
|
|
||||||
fontSize="sm"
|
|
||||||
>
|
|
||||||
<Icon as={BsPlus} boxSize={5} />
|
|
||||||
<Text>Add Filter</Text>
|
|
||||||
</HStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddFilterButton;
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { useCallback, useState } from "react";
|
|
||||||
import { HStack, IconButton, Input } from "@chakra-ui/react";
|
|
||||||
import { BsTrash } from "react-icons/bs";
|
|
||||||
|
|
||||||
import { type LogFilter } from "~/state/logFiltersSlice";
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
import { debounce } from "lodash-es";
|
|
||||||
import SelectFieldDropdown from "./SelectFieldDropdown";
|
|
||||||
import SelectComparatorDropdown from "./SelectComparatorDropdown";
|
|
||||||
|
|
||||||
const LogFilter = ({ filter }: { filter: LogFilter }) => {
|
|
||||||
const updateFilter = useAppStore((s) => s.logFilters.updateFilter);
|
|
||||||
const deleteFilter = useAppStore((s) => s.logFilters.deleteFilter);
|
|
||||||
|
|
||||||
const [editedValue, setEditedValue] = useState(filter.value);
|
|
||||||
|
|
||||||
const debouncedUpdateFilter = useCallback(
|
|
||||||
debounce((filter: LogFilter) => updateFilter(filter), 500, {
|
|
||||||
leading: true,
|
|
||||||
}),
|
|
||||||
[updateFilter],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HStack>
|
|
||||||
<SelectFieldDropdown filter={filter} />
|
|
||||||
<SelectComparatorDropdown filter={filter} />
|
|
||||||
<Input
|
|
||||||
value={editedValue}
|
|
||||||
onChange={(e) => {
|
|
||||||
setEditedValue(e.target.value);
|
|
||||||
debouncedUpdateFilter({ ...filter, value: e.target.value });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
aria-label="Delete Filter"
|
|
||||||
icon={<BsTrash />}
|
|
||||||
onClick={() => deleteFilter(filter.id)}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LogFilter;
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { VStack, Text } from "@chakra-ui/react";
|
|
||||||
|
|
||||||
import AddFilterButton from "./AddFilterButton";
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
import LogFilter from "./LogFilter";
|
|
||||||
|
|
||||||
const LogFilters = () => {
|
|
||||||
const filters = useAppStore((s) => s.logFilters.filters);
|
|
||||||
return (
|
|
||||||
<VStack
|
|
||||||
bgColor="white"
|
|
||||||
borderRadius={8}
|
|
||||||
borderWidth={1}
|
|
||||||
w="full"
|
|
||||||
alignItems="flex-start"
|
|
||||||
p={4}
|
|
||||||
spacing={4}
|
|
||||||
>
|
|
||||||
<Text fontWeight="bold" color="gray.500">
|
|
||||||
Filters
|
|
||||||
</Text>
|
|
||||||
{filters.map((filter) => (
|
|
||||||
<LogFilter key={filter.id} filter={filter} />
|
|
||||||
))}
|
|
||||||
<AddFilterButton />
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LogFilters;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { comparators, type LogFilter } from "~/state/logFiltersSlice";
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
import InputDropdown from "~/components/InputDropdown";
|
|
||||||
|
|
||||||
const SelectComparatorDropdown = ({ filter }: { filter: LogFilter }) => {
|
|
||||||
const updateFilter = useAppStore((s) => s.logFilters.updateFilter);
|
|
||||||
|
|
||||||
const { comparator } = filter;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InputDropdown
|
|
||||||
options={comparators}
|
|
||||||
selectedOption={comparator}
|
|
||||||
onSelect={(option) => updateFilter({ ...filter, comparator: option })}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SelectComparatorDropdown;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { defaultFilterableFields, type LogFilter } from "~/state/logFiltersSlice";
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
import { useTagNames } from "~/utils/hooks";
|
|
||||||
import InputDropdown from "~/components/InputDropdown";
|
|
||||||
|
|
||||||
const SelectFieldDropdown = ({ filter }: { filter: LogFilter }) => {
|
|
||||||
const tagNames = useTagNames().data;
|
|
||||||
|
|
||||||
const updateFilter = useAppStore((s) => s.logFilters.updateFilter);
|
|
||||||
|
|
||||||
const { field } = filter;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InputDropdown
|
|
||||||
options={[...defaultFilterableFields, ...(tagNames || [])]}
|
|
||||||
selectedOption={field}
|
|
||||||
onSelect={(option) => updateFilter({ ...filter, field: option })}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SelectFieldDropdown;
|
|
||||||
@@ -5,14 +5,14 @@ import { TableHeader, TableRow } from "./TableRow";
|
|||||||
|
|
||||||
export default function LoggedCallsTable() {
|
export default function LoggedCallsTable() {
|
||||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||||
const loggedCalls = useLoggedCalls().data;
|
const { data: loggedCalls } = useLoggedCalls();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card width="100%" overflowX="auto">
|
<Card width="100%" overflow="hidden">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader showCheckbox />
|
<TableHeader showCheckbox />
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{loggedCalls?.calls?.map((loggedCall) => {
|
{loggedCalls?.calls.map((loggedCall) => {
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={loggedCall.id}
|
key={loggedCall.id}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import Link from "next/link";
|
|||||||
import { type RouterOutputs } from "~/utils/api";
|
import { type RouterOutputs } from "~/utils/api";
|
||||||
import { FormattedJson } from "./FormattedJson";
|
import { FormattedJson } from "./FormattedJson";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
import { useLoggedCalls, useTagNames } from "~/utils/hooks";
|
import { useLoggedCalls } from "~/utils/hooks";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@@ -34,32 +34,27 @@ export const TableHeader = ({ showCheckbox }: { showCheckbox?: boolean }) => {
|
|||||||
const addAll = useAppStore((s) => s.selectedLogs.addSelectedLogIds);
|
const addAll = useAppStore((s) => s.selectedLogs.addSelectedLogIds);
|
||||||
const clearAll = useAppStore((s) => s.selectedLogs.clearSelectedLogIds);
|
const clearAll = useAppStore((s) => s.selectedLogs.clearSelectedLogIds);
|
||||||
const allSelected = useMemo(() => {
|
const allSelected = useMemo(() => {
|
||||||
if (!matchingLogIds || !matchingLogIds.length) return false;
|
if (!matchingLogIds) return false;
|
||||||
return matchingLogIds.every((id) => selectedLogIds.has(id));
|
return matchingLogIds.every((id) => selectedLogIds.has(id));
|
||||||
}, [selectedLogIds, matchingLogIds]);
|
}, [selectedLogIds, matchingLogIds]);
|
||||||
const tagNames = useTagNames().data;
|
|
||||||
return (
|
return (
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
{showCheckbox && (
|
{showCheckbox && (
|
||||||
<Th pr={0}>
|
<Th>
|
||||||
<HStack minW={16}>
|
<HStack w={8}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
isChecked={allSelected}
|
isChecked={allSelected}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
allSelected ? clearAll() : addAll(matchingLogIds || []);
|
allSelected ? clearAll() : addAll(matchingLogIds || []);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Text>
|
<Text>({selectedLogIds.size})</Text>
|
||||||
({selectedLogIds.size ? `${selectedLogIds.size}/` : ""}
|
|
||||||
{matchingLogIds?.length || 0})
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
</Th>
|
</Th>
|
||||||
)}
|
)}
|
||||||
<Th>Sent At</Th>
|
<Th>Time</Th>
|
||||||
<Th>Model</Th>
|
<Th>Model</Th>
|
||||||
{tagNames?.map((tagName) => <Th key={tagName}>{tagName}</Th>)}
|
|
||||||
<Th isNumeric>Duration</Th>
|
<Th isNumeric>Duration</Th>
|
||||||
<Th isNumeric>Input tokens</Th>
|
<Th isNumeric>Input tokens</Th>
|
||||||
<Th isNumeric>Output tokens</Th>
|
<Th isNumeric>Output tokens</Th>
|
||||||
@@ -81,14 +76,22 @@ export const TableRow = ({
|
|||||||
showCheckbox?: boolean;
|
showCheckbox?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const isError = loggedCall.modelResponse?.statusCode !== 200;
|
const isError = loggedCall.modelResponse?.statusCode !== 200;
|
||||||
const requestedAt = dayjs(loggedCall.requestedAt).format("MMMM D h:mm A");
|
const timeAgo = dayjs(loggedCall.requestedAt).fromNow();
|
||||||
const fullTime = dayjs(loggedCall.requestedAt).toString();
|
const fullTime = dayjs(loggedCall.requestedAt).toString();
|
||||||
|
|
||||||
|
const durationCell = (
|
||||||
|
<Td isNumeric>
|
||||||
|
{loggedCall.cacheHit ? (
|
||||||
|
<Text color="gray.500">Cached</Text>
|
||||||
|
) : (
|
||||||
|
((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2) + "s"
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
|
|
||||||
const isChecked = useAppStore((s) => s.selectedLogs.selectedLogIds.has(loggedCall.id));
|
const isChecked = useAppStore((s) => s.selectedLogs.selectedLogIds.has(loggedCall.id));
|
||||||
const toggleChecked = useAppStore((s) => s.selectedLogs.toggleSelectedLogId);
|
const toggleChecked = useAppStore((s) => s.selectedLogs.toggleSelectedLogId);
|
||||||
|
|
||||||
const tagNames = useTagNames().data;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tr
|
<Tr
|
||||||
@@ -98,7 +101,6 @@ export const TableRow = ({
|
|||||||
sx={{
|
sx={{
|
||||||
"> td": { borderBottom: "none" },
|
"> td": { borderBottom: "none" },
|
||||||
}}
|
}}
|
||||||
fontSize="sm"
|
|
||||||
>
|
>
|
||||||
{showCheckbox && (
|
{showCheckbox && (
|
||||||
<Td>
|
<Td>
|
||||||
@@ -108,11 +110,11 @@ export const TableRow = ({
|
|||||||
<Td>
|
<Td>
|
||||||
<Tooltip label={fullTime} placement="top">
|
<Tooltip label={fullTime} placement="top">
|
||||||
<Box whiteSpace="nowrap" minW="120px">
|
<Box whiteSpace="nowrap" minW="120px">
|
||||||
{requestedAt}
|
{timeAgo}
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td width="100%">
|
||||||
<HStack justifyContent="flex-start">
|
<HStack justifyContent="flex-start">
|
||||||
<Text
|
<Text
|
||||||
colorScheme="purple"
|
colorScheme="purple"
|
||||||
@@ -122,20 +124,12 @@ export const TableRow = ({
|
|||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
whiteSpace="nowrap"
|
|
||||||
>
|
>
|
||||||
{loggedCall.model}
|
{loggedCall.model}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Td>
|
</Td>
|
||||||
{tagNames?.map((tagName) => <Td key={tagName}>{loggedCall.tags[tagName]}</Td>)}
|
{durationCell}
|
||||||
<Td isNumeric>
|
|
||||||
{loggedCall.cacheHit ? (
|
|
||||||
<Text color="gray.500">Cached</Text>
|
|
||||||
) : (
|
|
||||||
((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2) + "s"
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric>{loggedCall.modelResponse?.inputTokens}</Td>
|
<Td isNumeric>{loggedCall.modelResponse?.inputTokens}</Td>
|
||||||
<Td isNumeric>{loggedCall.modelResponse?.outputTokens}</Td>
|
<Td isNumeric>{loggedCall.modelResponse?.outputTokens}</Td>
|
||||||
<Td sx={{ color: isError ? "red.500" : "green.500", fontWeight: "semibold" }} isNumeric>
|
<Td sx={{ color: isError ? "red.500" : "green.500", fontWeight: "semibold" }} isNumeric>
|
||||||
|
|||||||
@@ -21,15 +21,6 @@ export const env = createEnv({
|
|||||||
ANTHROPIC_API_KEY: z.string().default("placeholder"),
|
ANTHROPIC_API_KEY: z.string().default("placeholder"),
|
||||||
SENTRY_AUTH_TOKEN: z.string().optional(),
|
SENTRY_AUTH_TOKEN: z.string().optional(),
|
||||||
OPENPIPE_API_KEY: z.string().optional(),
|
OPENPIPE_API_KEY: z.string().optional(),
|
||||||
SENDER_EMAIL: z.string().default("placeholder"),
|
|
||||||
SMTP_HOST: z.string().default("placeholder"),
|
|
||||||
SMTP_PORT: z.string().default("placeholder"),
|
|
||||||
SMTP_LOGIN: z.string().default("placeholder"),
|
|
||||||
SMTP_PASSWORD: z.string().default("placeholder"),
|
|
||||||
WORKER_CONCURRENCY: z
|
|
||||||
.string()
|
|
||||||
.default("10")
|
|
||||||
.transform((val) => parseInt(val)),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,12 +58,6 @@ export const env = createEnv({
|
|||||||
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||||
OPENPIPE_API_KEY: process.env.OPENPIPE_API_KEY,
|
OPENPIPE_API_KEY: process.env.OPENPIPE_API_KEY,
|
||||||
NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS: process.env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS,
|
NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS: process.env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS,
|
||||||
SENDER_EMAIL: process.env.SENDER_EMAIL,
|
|
||||||
SMTP_HOST: process.env.SMTP_HOST,
|
|
||||||
SMTP_PORT: process.env.SMTP_PORT,
|
|
||||||
SMTP_LOGIN: process.env.SMTP_LOGIN,
|
|
||||||
SMTP_PASSWORD: process.env.SMTP_PASSWORD,
|
|
||||||
WORKER_CONCURRENCY: process.env.WORKER_CONCURRENCY,
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import openaiChatCompletionFrontend from "./openai-ChatCompletion/frontend";
|
import openaiChatCompletionFrontend from "./openai-ChatCompletion/frontend";
|
||||||
import replicateLlama2Frontend from "./replicate-llama2/frontend";
|
import replicateLlama2Frontend from "./replicate-llama2/frontend";
|
||||||
import anthropicFrontend from "./anthropic-completion/frontend";
|
import anthropicFrontend from "./anthropic-completion/frontend";
|
||||||
import openpipeFrontend from "./openpipe-chat/frontend";
|
|
||||||
import { type SupportedProvider, type FrontendModelProvider } from "./types";
|
import { type SupportedProvider, type FrontendModelProvider } from "./types";
|
||||||
|
|
||||||
// Keep attributes here that need to be accessible from the frontend. We can't
|
// Keep attributes here that need to be accessible from the frontend. We can't
|
||||||
@@ -11,7 +10,6 @@ const frontendModelProviders: Record<SupportedProvider, FrontendModelProvider<an
|
|||||||
"openai/ChatCompletion": openaiChatCompletionFrontend,
|
"openai/ChatCompletion": openaiChatCompletionFrontend,
|
||||||
"replicate/llama2": replicateLlama2Frontend,
|
"replicate/llama2": replicateLlama2Frontend,
|
||||||
"anthropic/completion": anthropicFrontend,
|
"anthropic/completion": anthropicFrontend,
|
||||||
"openpipe/Chat": openpipeFrontend,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default frontendModelProviders;
|
export default frontendModelProviders;
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import openaiChatCompletion from "./openai-ChatCompletion";
|
import openaiChatCompletion from "./openai-ChatCompletion";
|
||||||
import replicateLlama2 from "./replicate-llama2";
|
import replicateLlama2 from "./replicate-llama2";
|
||||||
import anthropicCompletion from "./anthropic-completion";
|
import anthropicCompletion from "./anthropic-completion";
|
||||||
import openpipeChatCompletion from "./openpipe-chat";
|
|
||||||
import { type SupportedProvider, type ModelProvider } from "./types";
|
import { type SupportedProvider, type ModelProvider } from "./types";
|
||||||
|
|
||||||
const modelProviders: Record<SupportedProvider, ModelProvider<any, any, any>> = {
|
const modelProviders: Record<SupportedProvider, ModelProvider<any, any, any>> = {
|
||||||
"openai/ChatCompletion": openaiChatCompletion,
|
"openai/ChatCompletion": openaiChatCompletion,
|
||||||
"replicate/llama2": replicateLlama2,
|
"replicate/llama2": replicateLlama2,
|
||||||
"anthropic/completion": anthropicCompletion,
|
"anthropic/completion": anthropicCompletion,
|
||||||
"openpipe/Chat": openpipeChatCompletion,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default modelProviders;
|
export default modelProviders;
|
||||||
|
|||||||
@@ -1,10 +1,54 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
import { isArray, isString } from "lodash-es";
|
import {
|
||||||
import { APIError } from "openai";
|
type ChatCompletionChunk,
|
||||||
import { type ChatCompletion, type CompletionCreateParams } from "openai/resources/chat";
|
type ChatCompletion,
|
||||||
import mergeChunks from "openpipe/src/openai/mergeChunks";
|
type CompletionCreateParams,
|
||||||
import { openai } from "~/server/utils/openai";
|
} from "openai/resources/chat";
|
||||||
import { type CompletionResponse } from "../types";
|
import { type CompletionResponse } from "../types";
|
||||||
|
import { isArray, isString, omit } from "lodash-es";
|
||||||
|
import { openai } from "~/server/utils/openai";
|
||||||
|
import { APIError } from "openai";
|
||||||
|
|
||||||
|
const mergeStreamedChunks = (
|
||||||
|
base: ChatCompletion | null,
|
||||||
|
chunk: ChatCompletionChunk,
|
||||||
|
): ChatCompletion => {
|
||||||
|
if (base === null) {
|
||||||
|
return mergeStreamedChunks({ ...chunk, choices: [] }, chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
const choices = [...base.choices];
|
||||||
|
for (const choice of chunk.choices) {
|
||||||
|
const baseChoice = choices.find((c) => c.index === choice.index);
|
||||||
|
if (baseChoice) {
|
||||||
|
baseChoice.finish_reason = choice.finish_reason ?? baseChoice.finish_reason;
|
||||||
|
baseChoice.message = baseChoice.message ?? { role: "assistant" };
|
||||||
|
|
||||||
|
if (choice.delta?.content)
|
||||||
|
baseChoice.message.content =
|
||||||
|
((baseChoice.message.content as string) ?? "") + (choice.delta.content ?? "");
|
||||||
|
if (choice.delta?.function_call) {
|
||||||
|
const fnCall = baseChoice.message.function_call ?? {};
|
||||||
|
fnCall.name =
|
||||||
|
((fnCall.name as string) ?? "") + ((choice.delta.function_call.name as string) ?? "");
|
||||||
|
fnCall.arguments =
|
||||||
|
((fnCall.arguments as string) ?? "") +
|
||||||
|
((choice.delta.function_call.arguments as string) ?? "");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// @ts-expect-error the types are correctly telling us that finish_reason
|
||||||
|
// could be null, but don't want to fix it right now.
|
||||||
|
choices.push({ ...omit(choice, "delta"), message: { role: "assistant", ...choice.delta } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged: ChatCompletion = {
|
||||||
|
...base,
|
||||||
|
choices,
|
||||||
|
};
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
};
|
||||||
|
|
||||||
export async function getCompletion(
|
export async function getCompletion(
|
||||||
input: CompletionCreateParams,
|
input: CompletionCreateParams,
|
||||||
@@ -15,6 +59,7 @@ export async function getCompletion(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (onStream) {
|
if (onStream) {
|
||||||
|
console.log("got started");
|
||||||
const resp = await openai.chat.completions.create(
|
const resp = await openai.chat.completions.create(
|
||||||
{ ...input, stream: true },
|
{ ...input, stream: true },
|
||||||
{
|
{
|
||||||
@@ -22,9 +67,11 @@ export async function getCompletion(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
for await (const part of resp) {
|
for await (const part of resp) {
|
||||||
finalCompletion = mergeChunks(finalCompletion, part);
|
console.log("got part", part);
|
||||||
|
finalCompletion = mergeStreamedChunks(finalCompletion, part);
|
||||||
onStream(finalCompletion);
|
onStream(finalCompletion);
|
||||||
}
|
}
|
||||||
|
console.log("got final", finalCompletion);
|
||||||
if (!finalCompletion) {
|
if (!finalCompletion) {
|
||||||
return {
|
return {
|
||||||
type: "error",
|
type: "error",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-4",
|
model: "gpt-4",
|
||||||
|
stream: true,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -28,6 +29,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-4",
|
model: "gpt-4",
|
||||||
|
stream: true,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -118,12 +120,13 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
"Convert to function call": {
|
"Convert to function call": {
|
||||||
icon: TfiThought,
|
icon: TfiThought,
|
||||||
description: "Use function calls to get output from the model in a more structured way.",
|
description: "Use function calls to get output from the model in a more structured way.",
|
||||||
instructions: `OpenAI functions are a specialized way for an LLM to return its final output.
|
instructions: `OpenAI functions are a specialized way for an LLM to return output.
|
||||||
|
|
||||||
Example 1 before:
|
This is what a prompt looks like before adding a function:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-4",
|
model: "gpt-4",
|
||||||
|
stream: true,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -136,10 +139,11 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
Example 1 after:
|
This is what one looks like after adding a function:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-4",
|
model: "gpt-4",
|
||||||
|
stream: true,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -152,7 +156,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
],
|
],
|
||||||
functions: [
|
functions: [
|
||||||
{
|
{
|
||||||
name: "log_extracted_sentiment",
|
name: "extract_sentiment",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object", // parameters must always be an object with a properties key
|
type: "object", // parameters must always be an object with a properties key
|
||||||
properties: { // properties key is required
|
properties: { // properties key is required
|
||||||
@@ -165,13 +169,13 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
function_call: {
|
function_call: {
|
||||||
name: "log_extracted_sentiment",
|
name: "extract_sentiment",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
=========
|
Here's another example of adding a function:
|
||||||
|
|
||||||
Example 2 before:
|
Before:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo",
|
model: "gpt-3.5-turbo",
|
||||||
@@ -193,7 +197,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
temperature: 0,
|
temperature: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
Example 2 after:
|
After:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo",
|
model: "gpt-3.5-turbo",
|
||||||
@@ -211,7 +215,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
temperature: 0,
|
temperature: 0,
|
||||||
functions: [
|
functions: [
|
||||||
{
|
{
|
||||||
name: "log_post_score",
|
name: "score_post",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -223,16 +227,17 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
function_call: {
|
function_call: {
|
||||||
name: "log_post_score",
|
name: "score_post",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
=========
|
Another example
|
||||||
|
|
||||||
Example 3 before:
|
Before:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo",
|
model: "gpt-3.5-turbo",
|
||||||
|
stream: true,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -241,7 +246,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
Example 3 after:
|
After:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo",
|
model: "gpt-3.5-turbo",
|
||||||
@@ -253,24 +258,21 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
],
|
],
|
||||||
functions: [
|
functions: [
|
||||||
{
|
{
|
||||||
name: "log_translated_text",
|
name: "write_in_language",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
translated_text: {
|
text: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "The text, written in the language specified in the prompt",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
function_call: {
|
function_call: {
|
||||||
name: "log_translated_text",
|
name: "write_in_language",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
=========
|
|
||||||
|
|
||||||
Add an OpenAI function that takes one or more nested parameters that match the expected output from this prompt.`,
|
Add an OpenAI function that takes one or more nested parameters that match the expected output from this prompt.`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
import { type OpenpipeChatOutput, type SupportedModel } from ".";
|
|
||||||
import { type FrontendModelProvider } from "../types";
|
|
||||||
import { refinementActions } from "./refinementActions";
|
|
||||||
import {
|
|
||||||
templateOpenOrcaPrompt,
|
|
||||||
templateAlpacaInstructPrompt,
|
|
||||||
// templateSystemUserAssistantPrompt,
|
|
||||||
templateInstructionInputResponsePrompt,
|
|
||||||
templateAiroborosPrompt,
|
|
||||||
templateGryphePrompt,
|
|
||||||
templateVicunaPrompt,
|
|
||||||
} from "./templatePrompt";
|
|
||||||
|
|
||||||
const frontendModelProvider: FrontendModelProvider<SupportedModel, OpenpipeChatOutput> = {
|
|
||||||
name: "OpenAI ChatCompletion",
|
|
||||||
|
|
||||||
models: {
|
|
||||||
"Open-Orca/OpenOrcaxOpenChat-Preview2-13B": {
|
|
||||||
name: "OpenOrcaxOpenChat-Preview2-13B",
|
|
||||||
contextWindow: 4096,
|
|
||||||
pricePerSecond: 0.0003,
|
|
||||||
speed: "medium",
|
|
||||||
provider: "openpipe/Chat",
|
|
||||||
learnMoreUrl: "https://huggingface.co/Open-Orca/OpenOrcaxOpenChat-Preview2-13B",
|
|
||||||
templatePrompt: templateOpenOrcaPrompt,
|
|
||||||
},
|
|
||||||
"Open-Orca/OpenOrca-Platypus2-13B": {
|
|
||||||
name: "OpenOrca-Platypus2-13B",
|
|
||||||
contextWindow: 4096,
|
|
||||||
pricePerSecond: 0.0003,
|
|
||||||
speed: "medium",
|
|
||||||
provider: "openpipe/Chat",
|
|
||||||
learnMoreUrl: "https://huggingface.co/Open-Orca/OpenOrca-Platypus2-13B",
|
|
||||||
templatePrompt: templateAlpacaInstructPrompt,
|
|
||||||
defaultStopTokens: ["</s>"],
|
|
||||||
},
|
|
||||||
// "stabilityai/StableBeluga-13B": {
|
|
||||||
// name: "StableBeluga-13B",
|
|
||||||
// contextWindow: 4096,
|
|
||||||
// pricePerSecond: 0.0003,
|
|
||||||
// speed: "medium",
|
|
||||||
// provider: "openpipe/Chat",
|
|
||||||
// learnMoreUrl: "https://huggingface.co/stabilityai/StableBeluga-13B",
|
|
||||||
// templatePrompt: templateSystemUserAssistantPrompt,
|
|
||||||
// },
|
|
||||||
"NousResearch/Nous-Hermes-Llama2-13b": {
|
|
||||||
name: "Nous-Hermes-Llama2-13b",
|
|
||||||
contextWindow: 4096,
|
|
||||||
pricePerSecond: 0.0003,
|
|
||||||
speed: "medium",
|
|
||||||
provider: "openpipe/Chat",
|
|
||||||
learnMoreUrl: "https://huggingface.co/NousResearch/Nous-Hermes-Llama2-13b",
|
|
||||||
templatePrompt: templateInstructionInputResponsePrompt,
|
|
||||||
},
|
|
||||||
"jondurbin/airoboros-l2-13b-gpt4-2.0": {
|
|
||||||
name: "airoboros-l2-13b-gpt4-2.0",
|
|
||||||
contextWindow: 4096,
|
|
||||||
pricePerSecond: 0.0003,
|
|
||||||
speed: "medium",
|
|
||||||
provider: "openpipe/Chat",
|
|
||||||
learnMoreUrl: "https://huggingface.co/jondurbin/airoboros-l2-13b-gpt4-2.0",
|
|
||||||
templatePrompt: templateAiroborosPrompt,
|
|
||||||
},
|
|
||||||
"lmsys/vicuna-13b-v1.5": {
|
|
||||||
name: "vicuna-13b-v1.5",
|
|
||||||
contextWindow: 4096,
|
|
||||||
pricePerSecond: 0.0003,
|
|
||||||
speed: "medium",
|
|
||||||
provider: "openpipe/Chat",
|
|
||||||
learnMoreUrl: "https://huggingface.co/lmsys/vicuna-13b-v1.5",
|
|
||||||
templatePrompt: templateVicunaPrompt,
|
|
||||||
},
|
|
||||||
"Gryphe/MythoMax-L2-13b": {
|
|
||||||
name: "MythoMax-L2-13b",
|
|
||||||
contextWindow: 4096,
|
|
||||||
pricePerSecond: 0.0003,
|
|
||||||
speed: "medium",
|
|
||||||
provider: "openpipe/Chat",
|
|
||||||
learnMoreUrl: "https://huggingface.co/Gryphe/MythoMax-L2-13b",
|
|
||||||
templatePrompt: templateGryphePrompt,
|
|
||||||
},
|
|
||||||
"NousResearch/Nous-Hermes-llama-2-7b": {
|
|
||||||
name: "Nous-Hermes-llama-2-7b",
|
|
||||||
contextWindow: 4096,
|
|
||||||
pricePerSecond: 0.0003,
|
|
||||||
speed: "medium",
|
|
||||||
provider: "openpipe/Chat",
|
|
||||||
learnMoreUrl: "https://huggingface.co/NousResearch/Nous-Hermes-llama-2-7b",
|
|
||||||
templatePrompt: templateInstructionInputResponsePrompt,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
refinementActions,
|
|
||||||
|
|
||||||
normalizeOutput: (output) => ({ type: "text", value: output }),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default frontendModelProvider;
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
||||||
import { isArray, isString } from "lodash-es";
|
|
||||||
import OpenAI, { APIError } from "openai";
|
|
||||||
|
|
||||||
import { type CompletionResponse } from "../types";
|
|
||||||
import { type OpenpipeChatInput, type OpenpipeChatOutput } from ".";
|
|
||||||
import frontendModelProvider from "./frontend";
|
|
||||||
|
|
||||||
const modelEndpoints: Record<OpenpipeChatInput["model"], string> = {
|
|
||||||
"Open-Orca/OpenOrcaxOpenChat-Preview2-13B": "https://5ef82gjxk8kdys-8000.proxy.runpod.net/v1",
|
|
||||||
"Open-Orca/OpenOrca-Platypus2-13B": "https://lt5qlel6qcji8t-8000.proxy.runpod.net/v1",
|
|
||||||
// "stabilityai/StableBeluga-13B": "https://vcorl8mxni2ou1-8000.proxy.runpod.net/v1",
|
|
||||||
"NousResearch/Nous-Hermes-Llama2-13b": "https://ncv8pw3u0vb8j2-8000.proxy.runpod.net/v1",
|
|
||||||
"jondurbin/airoboros-l2-13b-gpt4-2.0": "https://9nrbx7oph4btou-8000.proxy.runpod.net/v1",
|
|
||||||
"lmsys/vicuna-13b-v1.5": "https://h88hkt3ux73rb7-8000.proxy.runpod.net/v1",
|
|
||||||
"Gryphe/MythoMax-L2-13b": "https://3l5jvhnxdgky3v-8000.proxy.runpod.net/v1",
|
|
||||||
"NousResearch/Nous-Hermes-llama-2-7b": "https://ua1bpc6kv3dgge-8000.proxy.runpod.net/v1",
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getCompletion(
|
|
||||||
input: OpenpipeChatInput,
|
|
||||||
onStream: ((partialOutput: OpenpipeChatOutput) => void) | null,
|
|
||||||
): Promise<CompletionResponse<OpenpipeChatOutput>> {
|
|
||||||
const { model, messages, ...rest } = input;
|
|
||||||
|
|
||||||
const templatedPrompt = frontendModelProvider.models[model].templatePrompt?.(messages);
|
|
||||||
|
|
||||||
if (!templatedPrompt) {
|
|
||||||
return {
|
|
||||||
type: "error",
|
|
||||||
message: "Failed to generate prompt",
|
|
||||||
autoRetry: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const openai = new OpenAI({
|
|
||||||
baseURL: modelEndpoints[model],
|
|
||||||
});
|
|
||||||
const start = Date.now();
|
|
||||||
let finalCompletion: OpenpipeChatOutput = "";
|
|
||||||
|
|
||||||
const completionParams = {
|
|
||||||
model,
|
|
||||||
prompt: templatedPrompt,
|
|
||||||
...rest,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!completionParams.stop && frontendModelProvider.models[model].defaultStopTokens) {
|
|
||||||
completionParams.stop = frontendModelProvider.models[model].defaultStopTokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (onStream) {
|
|
||||||
const resp = await openai.completions.create(
|
|
||||||
{ ...completionParams, stream: true },
|
|
||||||
{
|
|
||||||
maxRetries: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
for await (const part of resp) {
|
|
||||||
finalCompletion += part.choices[0]?.text;
|
|
||||||
onStream(finalCompletion);
|
|
||||||
}
|
|
||||||
if (!finalCompletion) {
|
|
||||||
return {
|
|
||||||
type: "error",
|
|
||||||
message: "Streaming failed to return a completion",
|
|
||||||
autoRetry: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const resp = await openai.completions.create(
|
|
||||||
{ ...completionParams, stream: false },
|
|
||||||
{
|
|
||||||
maxRetries: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
finalCompletion = resp.choices[0]?.text || "";
|
|
||||||
if (!finalCompletion) {
|
|
||||||
return {
|
|
||||||
type: "error",
|
|
||||||
message: "Failed to return a completion",
|
|
||||||
autoRetry: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const timeToComplete = Date.now() - start;
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "success",
|
|
||||||
statusCode: 200,
|
|
||||||
value: finalCompletion,
|
|
||||||
timeToComplete,
|
|
||||||
};
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof APIError) {
|
|
||||||
// The types from the sdk are wrong
|
|
||||||
const rawMessage = error.message as string | string[];
|
|
||||||
// If the message is not a string, stringify it
|
|
||||||
const message = isString(rawMessage)
|
|
||||||
? rawMessage
|
|
||||||
: isArray(rawMessage)
|
|
||||||
? rawMessage.map((m) => m.toString()).join("\n")
|
|
||||||
: (rawMessage as any).toString();
|
|
||||||
return {
|
|
||||||
type: "error",
|
|
||||||
message,
|
|
||||||
autoRetry: error.status === 429 || error.status === 503,
|
|
||||||
statusCode: error.status,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.error(error);
|
|
||||||
return {
|
|
||||||
type: "error",
|
|
||||||
message: (error as Error).message,
|
|
||||||
autoRetry: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { type JSONSchema4 } from "json-schema";
|
|
||||||
import { type ModelProvider } from "../types";
|
|
||||||
import inputSchema from "./input.schema.json";
|
|
||||||
import { getCompletion } from "./getCompletion";
|
|
||||||
import frontendModelProvider from "./frontend";
|
|
||||||
|
|
||||||
const supportedModels = [
|
|
||||||
"Open-Orca/OpenOrcaxOpenChat-Preview2-13B",
|
|
||||||
"Open-Orca/OpenOrca-Platypus2-13B",
|
|
||||||
// "stabilityai/StableBeluga-13B",
|
|
||||||
"NousResearch/Nous-Hermes-Llama2-13b",
|
|
||||||
"jondurbin/airoboros-l2-13b-gpt4-2.0",
|
|
||||||
"lmsys/vicuna-13b-v1.5",
|
|
||||||
"Gryphe/MythoMax-L2-13b",
|
|
||||||
"NousResearch/Nous-Hermes-llama-2-7b",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type SupportedModel = (typeof supportedModels)[number];
|
|
||||||
|
|
||||||
export type OpenpipeChatInput = {
|
|
||||||
model: SupportedModel;
|
|
||||||
messages: {
|
|
||||||
role: "system" | "user" | "assistant";
|
|
||||||
content: string;
|
|
||||||
}[];
|
|
||||||
temperature?: number;
|
|
||||||
top_p?: number;
|
|
||||||
stop?: string[] | string;
|
|
||||||
max_tokens?: number;
|
|
||||||
presence_penalty?: number;
|
|
||||||
frequency_penalty?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OpenpipeChatOutput = string;
|
|
||||||
|
|
||||||
export type OpenpipeChatModelProvider = ModelProvider<
|
|
||||||
SupportedModel,
|
|
||||||
OpenpipeChatInput,
|
|
||||||
OpenpipeChatOutput
|
|
||||||
>;
|
|
||||||
|
|
||||||
const modelProvider: OpenpipeChatModelProvider = {
|
|
||||||
getModel: (input) => input.model,
|
|
||||||
inputSchema: inputSchema as JSONSchema4,
|
|
||||||
canStream: true,
|
|
||||||
getCompletion,
|
|
||||||
getUsage: (input, output) => {
|
|
||||||
// TODO: Implement this
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
...frontendModelProvider,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default modelProvider;
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"model": {
|
|
||||||
"description": "ID of the model to use.",
|
|
||||||
"example": "Open-Orca/OpenOrcaxOpenChat-Preview2-13B",
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"Open-Orca/OpenOrcaxOpenChat-Preview2-13B",
|
|
||||||
"Open-Orca/OpenOrca-Platypus2-13B",
|
|
||||||
"NousResearch/Nous-Hermes-Llama2-13b",
|
|
||||||
"jondurbin/airoboros-l2-13b-gpt4-2.0",
|
|
||||||
"lmsys/vicuna-13b-v1.5",
|
|
||||||
"Gryphe/MythoMax-L2-13b",
|
|
||||||
"NousResearch/Nous-Hermes-llama-2-7b"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"description": "A list of messages comprising the conversation so far.",
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"role": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["system", "user", "assistant"],
|
|
||||||
"description": "The role of the messages author. One of `system`, `user`, or `assistant`."
|
|
||||||
},
|
|
||||||
"content": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The contents of the message. `content` is required for all messages."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["role", "content"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"temperature": {
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 0,
|
|
||||||
"maximum": 2,
|
|
||||||
"default": 1,
|
|
||||||
"example": 1,
|
|
||||||
"nullable": true,
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"top_p": {
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 0,
|
|
||||||
"maximum": 1,
|
|
||||||
"default": 1,
|
|
||||||
"example": 1,
|
|
||||||
"nullable": true,
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"stop": {
|
|
||||||
"description": "Up to 4 sequences where the API will stop generating further tokens.\n",
|
|
||||||
"default": null,
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"maxItems": 4,
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"max_tokens": {
|
|
||||||
"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",
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"presence_penalty": {
|
|
||||||
"type": "number",
|
|
||||||
"default": 0,
|
|
||||||
"minimum": -2,
|
|
||||||
"maximum": 2,
|
|
||||||
"nullable": true,
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"frequency_penalty": {
|
|
||||||
"type": "number",
|
|
||||||
"default": 0,
|
|
||||||
"minimum": -2,
|
|
||||||
"maximum": 2,
|
|
||||||
"nullable": true,
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["model", "messages"]
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { type RefinementAction } from "../types";
|
|
||||||
|
|
||||||
export const refinementActions: Record<string, RefinementAction> = {};
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
import { type OpenpipeChatInput } from ".";
|
|
||||||
|
|
||||||
// User: Hello<|end_of_turn|>Assistant: Hi<|end_of_turn|>User: How are you today?<|end_of_turn|>Assistant:
|
|
||||||
export const templateOpenOrcaPrompt = (messages: OpenpipeChatInput["messages"]) => {
|
|
||||||
const splitter = "<|end_of_turn|>";
|
|
||||||
|
|
||||||
const formattedMessages = messages.map((message) => {
|
|
||||||
if (message.role === "system" || message.role === "user") {
|
|
||||||
return "User: " + message.content;
|
|
||||||
} else {
|
|
||||||
return "Assistant: " + message.content;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let prompt = formattedMessages.join(splitter);
|
|
||||||
|
|
||||||
// Ensure that the prompt ends with an assistant message
|
|
||||||
const lastUserIndex = prompt.lastIndexOf("User:");
|
|
||||||
const lastAssistantIndex = prompt.lastIndexOf("Assistant:");
|
|
||||||
if (lastUserIndex > lastAssistantIndex) {
|
|
||||||
prompt += splitter + "Assistant:";
|
|
||||||
}
|
|
||||||
|
|
||||||
return prompt;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ### Instruction:
|
|
||||||
|
|
||||||
// <prompt> (without the <>)
|
|
||||||
|
|
||||||
// ### Response: (leave two newlines for model to respond)
|
|
||||||
export const templateAlpacaInstructPrompt = (messages: OpenpipeChatInput["messages"]) => {
|
|
||||||
const splitter = "\n\n";
|
|
||||||
|
|
||||||
const userTag = "### Instruction:\n\n";
|
|
||||||
const assistantTag = "### Response:\n\n";
|
|
||||||
|
|
||||||
const formattedMessages = messages.map((message) => {
|
|
||||||
if (message.role === "system" || message.role === "user") {
|
|
||||||
return userTag + message.content;
|
|
||||||
} else {
|
|
||||||
return assistantTag + message.content;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let prompt = formattedMessages.join(splitter);
|
|
||||||
|
|
||||||
// Ensure that the prompt ends with an assistant message
|
|
||||||
const lastUserIndex = prompt.lastIndexOf(userTag);
|
|
||||||
const lastAssistantIndex = prompt.lastIndexOf(assistantTag);
|
|
||||||
if (lastUserIndex > lastAssistantIndex) {
|
|
||||||
prompt += splitter + assistantTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
return prompt;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ### System:
|
|
||||||
// This is a system prompt, please behave and help the user.
|
|
||||||
|
|
||||||
// ### User:
|
|
||||||
// Your prompt here
|
|
||||||
|
|
||||||
// ### Assistant
|
|
||||||
// The output of Stable Beluga 13B
|
|
||||||
export const templateSystemUserAssistantPrompt = (messages: OpenpipeChatInput["messages"]) => {
|
|
||||||
const splitter = "\n\n";
|
|
||||||
|
|
||||||
const systemTag = "### System:\n";
|
|
||||||
const userTag = "### User:\n";
|
|
||||||
const assistantTag = "### Assistant\n";
|
|
||||||
|
|
||||||
const formattedMessages = messages.map((message) => {
|
|
||||||
if (message.role === "system") {
|
|
||||||
return systemTag + message.content;
|
|
||||||
} else if (message.role === "user") {
|
|
||||||
return userTag + message.content;
|
|
||||||
} else {
|
|
||||||
return assistantTag + message.content;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let prompt = formattedMessages.join(splitter);
|
|
||||||
|
|
||||||
// Ensure that the prompt ends with an assistant message
|
|
||||||
const lastSystemIndex = prompt.lastIndexOf(systemTag);
|
|
||||||
const lastUserIndex = prompt.lastIndexOf(userTag);
|
|
||||||
const lastAssistantIndex = prompt.lastIndexOf(assistantTag);
|
|
||||||
if (lastSystemIndex > lastAssistantIndex || lastUserIndex > lastAssistantIndex) {
|
|
||||||
prompt += splitter + assistantTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
return prompt;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ### Instruction:
|
|
||||||
// <prompt>
|
|
||||||
|
|
||||||
// ### Input:
|
|
||||||
// <additional context>
|
|
||||||
|
|
||||||
// ### Response:
|
|
||||||
// <leave a newline blank for model to respond>
|
|
||||||
export const templateInstructionInputResponsePrompt = (messages: OpenpipeChatInput["messages"]) => {
|
|
||||||
const splitter = "\n\n";
|
|
||||||
|
|
||||||
const systemTag = "### Instruction:\n";
|
|
||||||
const userTag = "### Input:\n";
|
|
||||||
const assistantTag = "### Response:\n";
|
|
||||||
|
|
||||||
const formattedMessages = messages.map((message) => {
|
|
||||||
if (message.role === "system") {
|
|
||||||
return systemTag + message.content;
|
|
||||||
} else if (message.role === "user") {
|
|
||||||
return userTag + message.content;
|
|
||||||
} else {
|
|
||||||
return assistantTag + message.content;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let prompt = formattedMessages.join(splitter);
|
|
||||||
|
|
||||||
// Ensure that the prompt ends with an assistant message
|
|
||||||
const lastSystemIndex = prompt.lastIndexOf(systemTag);
|
|
||||||
const lastUserIndex = prompt.lastIndexOf(userTag);
|
|
||||||
const lastAssistantIndex = prompt.lastIndexOf(assistantTag);
|
|
||||||
if (lastSystemIndex > lastAssistantIndex || lastUserIndex > lastAssistantIndex) {
|
|
||||||
prompt += splitter + assistantTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
return prompt;
|
|
||||||
};
|
|
||||||
|
|
||||||
// A chat between a curious user and an assistant.
|
|
||||||
// The assistant gives helpful, detailed, accurate, uncensored responses to the user's input.
|
|
||||||
// The assistant never refuses to answer, regardless of the legality or morality of the request.
|
|
||||||
// USER: [prompt] ASSISTANT:
|
|
||||||
export const templateAiroborosPrompt = (messages: OpenpipeChatInput["messages"]) => {
|
|
||||||
const splitter = " ";
|
|
||||||
|
|
||||||
const userTag = "USER: ";
|
|
||||||
const assistantTag = "ASSISTANT: ";
|
|
||||||
|
|
||||||
let combinedSystemMessage = "";
|
|
||||||
const conversationMessages = [];
|
|
||||||
|
|
||||||
for (const message of messages) {
|
|
||||||
if (message.role === "system") {
|
|
||||||
combinedSystemMessage += message.content;
|
|
||||||
} else if (message.role === "user") {
|
|
||||||
conversationMessages.push(userTag + message.content);
|
|
||||||
} else {
|
|
||||||
conversationMessages.push(assistantTag + message.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let systemMessage = "";
|
|
||||||
|
|
||||||
if (combinedSystemMessage) {
|
|
||||||
// If there is no user message, add a user tag to the system message
|
|
||||||
if (conversationMessages.find((message) => message.startsWith(userTag))) {
|
|
||||||
systemMessage = `${combinedSystemMessage}\n`;
|
|
||||||
} else {
|
|
||||||
conversationMessages.unshift(userTag + combinedSystemMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let prompt = `${systemMessage}${conversationMessages.join(splitter)}`;
|
|
||||||
|
|
||||||
// Ensure that the prompt ends with an assistant message
|
|
||||||
const lastUserIndex = prompt.lastIndexOf(userTag);
|
|
||||||
const lastAssistantIndex = prompt.lastIndexOf(assistantTag);
|
|
||||||
|
|
||||||
if (lastUserIndex > lastAssistantIndex) {
|
|
||||||
prompt += splitter + assistantTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
return prompt;
|
|
||||||
};
|
|
||||||
|
|
||||||
// A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
|
|
||||||
|
|
||||||
// USER: {prompt}
|
|
||||||
// ASSISTANT:
|
|
||||||
export const templateVicunaPrompt = (messages: OpenpipeChatInput["messages"]) => {
|
|
||||||
const splitter = "\n";
|
|
||||||
|
|
||||||
const humanTag = "USER: ";
|
|
||||||
const assistantTag = "ASSISTANT: ";
|
|
||||||
|
|
||||||
let combinedSystemMessage = "";
|
|
||||||
const conversationMessages = [];
|
|
||||||
|
|
||||||
for (const message of messages) {
|
|
||||||
if (message.role === "system") {
|
|
||||||
combinedSystemMessage += message.content;
|
|
||||||
} else if (message.role === "user") {
|
|
||||||
conversationMessages.push(humanTag + message.content);
|
|
||||||
} else {
|
|
||||||
conversationMessages.push(assistantTag + message.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let systemMessage = "";
|
|
||||||
|
|
||||||
if (combinedSystemMessage) {
|
|
||||||
// If there is no user message, add a user tag to the system message
|
|
||||||
if (conversationMessages.find((message) => message.startsWith(humanTag))) {
|
|
||||||
systemMessage = `${combinedSystemMessage}\n\n`;
|
|
||||||
} else {
|
|
||||||
conversationMessages.unshift(humanTag + combinedSystemMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let prompt = `${systemMessage}${conversationMessages.join(splitter)}`;
|
|
||||||
|
|
||||||
// Ensure that the prompt ends with an assistant message
|
|
||||||
const lastHumanIndex = prompt.lastIndexOf(humanTag);
|
|
||||||
const lastAssistantIndex = prompt.lastIndexOf(assistantTag);
|
|
||||||
if (lastHumanIndex > lastAssistantIndex) {
|
|
||||||
prompt += splitter + assistantTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
return prompt.trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
// <System prompt/Character Card>
|
|
||||||
|
|
||||||
// ### Instruction:
|
|
||||||
// Your instruction or question here.
|
|
||||||
// For roleplay purposes, I suggest the following - Write <CHAR NAME>'s next reply in a chat between <YOUR NAME> and <CHAR NAME>. Write a single reply only.
|
|
||||||
|
|
||||||
// ### Response:
|
|
||||||
export const templateGryphePrompt = (messages: OpenpipeChatInput["messages"]) => {
|
|
||||||
const splitter = "\n\n";
|
|
||||||
|
|
||||||
const instructionTag = "### Instruction:\n";
|
|
||||||
const responseTag = "### Response:\n";
|
|
||||||
|
|
||||||
let combinedSystemMessage = "";
|
|
||||||
const conversationMessages = [];
|
|
||||||
|
|
||||||
for (const message of messages) {
|
|
||||||
if (message.role === "system") {
|
|
||||||
combinedSystemMessage += message.content;
|
|
||||||
} else if (message.role === "user") {
|
|
||||||
conversationMessages.push(instructionTag + message.content);
|
|
||||||
} else {
|
|
||||||
conversationMessages.push(responseTag + message.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let systemMessage = "";
|
|
||||||
|
|
||||||
if (combinedSystemMessage) {
|
|
||||||
// If there is no user message, add a user tag to the system message
|
|
||||||
if (conversationMessages.find((message) => message.startsWith(instructionTag))) {
|
|
||||||
systemMessage = `${combinedSystemMessage}\n\n`;
|
|
||||||
} else {
|
|
||||||
conversationMessages.unshift(instructionTag + combinedSystemMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let prompt = `${systemMessage}${conversationMessages.join(splitter)}`;
|
|
||||||
|
|
||||||
// Ensure that the prompt ends with an assistant message
|
|
||||||
const lastInstructionIndex = prompt.lastIndexOf(instructionTag);
|
|
||||||
const lastAssistantIndex = prompt.lastIndexOf(responseTag);
|
|
||||||
if (lastInstructionIndex > lastAssistantIndex) {
|
|
||||||
prompt += splitter + responseTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
return prompt;
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,7 @@ const replicate = new Replicate({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const modelIds: Record<ReplicateLlama2Input["model"], string> = {
|
const modelIds: Record<ReplicateLlama2Input["model"], string> = {
|
||||||
"7b-chat": "7b0bfc9aff140d5b75bacbed23e91fd3c34b01a1e958d32132de6e0a19796e2c",
|
"7b-chat": "4f0b260b6a13eb53a6b1891f089d57c08f41003ae79458be5011303d81a394dc",
|
||||||
"13b-chat": "2a7f981751ec7fdf87b5b91ad4db53683a98082e9ff7bfd12c8cd5ea85980a52",
|
"13b-chat": "2a7f981751ec7fdf87b5b91ad4db53683a98082e9ff7bfd12c8cd5ea85980a52",
|
||||||
"70b-chat": "2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1",
|
"70b-chat": "2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ import { type JSONSchema4 } from "json-schema";
|
|||||||
import { type IconType } from "react-icons";
|
import { type IconType } from "react-icons";
|
||||||
import { type JsonValue } from "type-fest";
|
import { type JsonValue } from "type-fest";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { type OpenpipeChatInput } from "./openpipe-chat";
|
|
||||||
|
|
||||||
export const ZodSupportedProvider = z.union([
|
export const ZodSupportedProvider = z.union([
|
||||||
z.literal("openai/ChatCompletion"),
|
z.literal("openai/ChatCompletion"),
|
||||||
z.literal("replicate/llama2"),
|
z.literal("replicate/llama2"),
|
||||||
z.literal("anthropic/completion"),
|
z.literal("anthropic/completion"),
|
||||||
z.literal("openpipe/Chat"),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type SupportedProvider = z.infer<typeof ZodSupportedProvider>;
|
export type SupportedProvider = z.infer<typeof ZodSupportedProvider>;
|
||||||
@@ -24,8 +22,6 @@ export type Model = {
|
|||||||
description?: string;
|
description?: string;
|
||||||
learnMoreUrl?: string;
|
learnMoreUrl?: string;
|
||||||
apiDocsUrl?: string;
|
apiDocsUrl?: string;
|
||||||
templatePrompt?: (initialPrompt: OpenpipeChatInput["messages"]) => string;
|
|
||||||
defaultStopTokens?: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProviderModel = { provider: z.infer<typeof ZodSupportedProvider>; model: string };
|
export type ProviderModel = { provider: z.infer<typeof ZodSupportedProvider>; model: string };
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import { Card, Table, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { isDate, isObject, isString } from "lodash-es";
|
|
||||||
import AppShell from "~/components/nav/AppShell";
|
|
||||||
import { type RouterOutputs, api } from "~/utils/api";
|
|
||||||
|
|
||||||
const fieldsToShow: (keyof RouterOutputs["adminJobs"]["list"][0])[] = [
|
|
||||||
"id",
|
|
||||||
"queue_name",
|
|
||||||
"payload",
|
|
||||||
"priority",
|
|
||||||
"attempts",
|
|
||||||
"last_error",
|
|
||||||
"created_at",
|
|
||||||
"key",
|
|
||||||
"locked_at",
|
|
||||||
"run_at",
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Jobs() {
|
|
||||||
const jobs = api.adminJobs.list.useQuery({});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShell title="Admin Jobs">
|
|
||||||
<Card m={4} overflowX="auto">
|
|
||||||
<Table>
|
|
||||||
<Thead>
|
|
||||||
<Tr>
|
|
||||||
{fieldsToShow.map((field) => (
|
|
||||||
<Th key={field}>{field}</Th>
|
|
||||||
))}
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>
|
|
||||||
{jobs.data?.map((job) => (
|
|
||||||
<Tr key={job.id}>
|
|
||||||
{fieldsToShow.map((field) => {
|
|
||||||
// Check if object
|
|
||||||
let value = job[field];
|
|
||||||
if (isDate(value)) {
|
|
||||||
value = dayjs(value).format("YYYY-MM-DD HH:mm:ss");
|
|
||||||
} else if (isObject(value) && !isString(value)) {
|
|
||||||
value = JSON.stringify(value);
|
|
||||||
} // check if date
|
|
||||||
return <Td key={field}>{value}</Td>;
|
|
||||||
})}
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</Card>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||||
import cors from "nextjs-cors";
|
import cors from "nextjs-cors";
|
||||||
import { createOpenApiNextHandler } from "trpc-openapi";
|
import { createOpenApiNextHandler } from "trpc-openapi";
|
||||||
import { v1ApiRouter } from "~/server/api/external/v1Api.router";
|
import { createProcedureCache } from "trpc-openapi/dist/adapters/node-http/procedures";
|
||||||
import { createOpenApiContext } from "~/server/api/external/openApiTrpc";
|
import { appRouter } from "~/server/api/root.router";
|
||||||
|
import { createTRPCContext } from "~/server/api/trpc";
|
||||||
|
|
||||||
const openApiHandler = createOpenApiNextHandler({
|
const openApiHandler = createOpenApiNextHandler({
|
||||||
router: v1ApiRouter,
|
router: appRouter,
|
||||||
createContext: createOpenApiContext,
|
createContext: createTRPCContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cache = createProcedureCache(appRouter);
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
// Setup CORS
|
// Setup CORS
|
||||||
await cors(req, res);
|
await cors(req, res);
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||||
import { generateOpenApiDocument } from "trpc-openapi";
|
import { generateOpenApiDocument } from "trpc-openapi";
|
||||||
import { v1ApiRouter } from "~/server/api/external/v1Api.router";
|
import { appRouter } from "~/server/api/root.router";
|
||||||
|
|
||||||
export const openApiDocument = generateOpenApiDocument(v1ApiRouter, {
|
export const openApiDocument = generateOpenApiDocument(appRouter, {
|
||||||
title: "OpenPipe API",
|
title: "OpenPipe API",
|
||||||
description: "The public API for reporting API calls to OpenPipe",
|
description: "The public API for reporting API calls to OpenPipe",
|
||||||
version: "0.1.1",
|
version: "0.1.0",
|
||||||
baseUrl: "https://app.openpipe.ai/api/v1",
|
baseUrl: "https://app.openpipe.ai/api",
|
||||||
});
|
});
|
||||||
// Respond with our OpenAPI schema
|
// Respond with our OpenAPI schema
|
||||||
const hander = (req: NextApiRequest, res: NextApiResponse) => {
|
const hander = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
6
app/src/pages/api/sentry-example-api.js
Normal file
6
app/src/pages/api/sentry-example-api.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// A faulty API route to test Sentry's error monitoring
|
||||||
|
// @ts-expect-error just a test file, don't care about types
|
||||||
|
export default function handler(_req, res) {
|
||||||
|
throw new Error("Sentry Example API Route Error");
|
||||||
|
res.status(200).json({ name: "John Doe" });
|
||||||
|
}
|
||||||
@@ -26,6 +26,26 @@ import Head from "next/head";
|
|||||||
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
|
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
|
||||||
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
|
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;
|
||||||
|
|
||||||
|
// const helpers = createServerSideHelpers({
|
||||||
|
// router: appRouter,
|
||||||
|
// ctx: createInnerTRPCContext({ session: null }),
|
||||||
|
// transformer: superjson, // optional - adds superjson serialization
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // prefetch query
|
||||||
|
// await helpers.experiments.stats.prefetch({ id: experimentId });
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// props: {
|
||||||
|
// trpcState: helpers.dehydrate(),
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
export default function Experiment() {
|
export default function Experiment() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
@@ -33,9 +53,9 @@ export default function Experiment() {
|
|||||||
|
|
||||||
const experiment = useExperiment();
|
const experiment = useExperiment();
|
||||||
const experimentStats = api.experiments.stats.useQuery(
|
const experimentStats = api.experiments.stats.useQuery(
|
||||||
{ id: experiment.data?.id as string },
|
{ id: router.query.id as string },
|
||||||
{
|
{
|
||||||
enabled: !!experiment.data?.id,
|
enabled: !!router.query.id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const stats = experimentStats.data;
|
const stats = experimentStats.data;
|
||||||
@@ -124,8 +144,8 @@ export default function Experiment() {
|
|||||||
<ExperimentHeaderButtons />
|
<ExperimentHeaderButtons />
|
||||||
</PageHeaderContainer>
|
</PageHeaderContainer>
|
||||||
<ExperimentSettingsDrawer />
|
<ExperimentSettingsDrawer />
|
||||||
<Box w="100%" overflowX="auto" flex={1} id="output-container">
|
<Box w="100%" overflowX="auto" flex={1}>
|
||||||
<OutputsTable experimentId={experiment.data?.id} />
|
<OutputsTable experimentId={router.query.id as string | undefined} />
|
||||||
</Box>
|
</Box>
|
||||||
</VStack>
|
</VStack>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import { Center, Text, VStack, HStack, Button, Card } from "@chakra-ui/react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import AppShell from "~/components/nav/AppShell";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
import { useSyncVariantEditor } from "~/state/sync";
|
|
||||||
import { maybeReportError } from "~/utils/errorHandling/maybeReportError";
|
|
||||||
|
|
||||||
export default function Invitation() {
|
|
||||||
const router = useRouter();
|
|
||||||
const utils = api.useContext();
|
|
||||||
useSyncVariantEditor();
|
|
||||||
|
|
||||||
const setSelectedProjectId = useAppStore((state) => state.setSelectedProjectId);
|
|
||||||
|
|
||||||
const invitationToken = router.query.invitationToken as string | undefined;
|
|
||||||
|
|
||||||
const invitation = api.users.getProjectInvitation.useQuery(
|
|
||||||
{ invitationToken: invitationToken as string },
|
|
||||||
{ enabled: !!invitationToken },
|
|
||||||
);
|
|
||||||
|
|
||||||
const cancelMutation = api.users.cancelProjectInvitation.useMutation();
|
|
||||||
const [declineInvitation, isDeclining] = useHandledAsyncCallback(async () => {
|
|
||||||
if (invitationToken) {
|
|
||||||
await cancelMutation.mutateAsync({
|
|
||||||
invitationToken,
|
|
||||||
});
|
|
||||||
await router.replace("/");
|
|
||||||
}
|
|
||||||
}, [cancelMutation, invitationToken]);
|
|
||||||
|
|
||||||
const acceptMutation = api.users.acceptProjectInvitation.useMutation();
|
|
||||||
const [acceptInvitation, isAccepting] = useHandledAsyncCallback(async () => {
|
|
||||||
if (invitationToken) {
|
|
||||||
const resp = await acceptMutation.mutateAsync({
|
|
||||||
invitationToken,
|
|
||||||
});
|
|
||||||
if (!maybeReportError(resp) && resp) {
|
|
||||||
await utils.projects.list.invalidate();
|
|
||||||
setSelectedProjectId(resp.payload);
|
|
||||||
}
|
|
||||||
await router.replace("/");
|
|
||||||
}
|
|
||||||
}, [acceptMutation, invitationToken]);
|
|
||||||
|
|
||||||
if (invitation.isLoading) {
|
|
||||||
return (
|
|
||||||
<AppShell requireAuth title="Loading...">
|
|
||||||
<Center h="full">
|
|
||||||
<Text>Loading...</Text>
|
|
||||||
</Center>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!invitationToken || !invitation.data) {
|
|
||||||
return (
|
|
||||||
<AppShell requireAuth title="Invalid invitation token">
|
|
||||||
<Center h="full">
|
|
||||||
<Text>
|
|
||||||
The invitation you've received is invalid or expired. Please ask your project admin for
|
|
||||||
a new token.
|
|
||||||
</Text>
|
|
||||||
</Center>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AppShell requireAuth title="Invitation">
|
|
||||||
<Center h="full">
|
|
||||||
<Card>
|
|
||||||
<VStack
|
|
||||||
spacing={8}
|
|
||||||
w="full"
|
|
||||||
maxW="2xl"
|
|
||||||
p={16}
|
|
||||||
borderWidth={1}
|
|
||||||
borderRadius={8}
|
|
||||||
bgColor="white"
|
|
||||||
>
|
|
||||||
<Text fontSize="lg" fontWeight="bold">
|
|
||||||
You're invited! 🎉
|
|
||||||
</Text>
|
|
||||||
<Text textAlign="center">
|
|
||||||
You've been invited to join <b>{invitation.data.project.name}</b> by{" "}
|
|
||||||
<b>
|
|
||||||
{invitation.data.sender.name} ({invitation.data.sender.email})
|
|
||||||
</b>
|
|
||||||
.
|
|
||||||
</Text>
|
|
||||||
<HStack spacing={4}>
|
|
||||||
<Button colorScheme="gray" isLoading={isDeclining} onClick={declineInvitation}>
|
|
||||||
Decline
|
|
||||||
</Button>
|
|
||||||
<Button colorScheme="orange" isLoading={isAccepting} onClick={acceptInvitation}>
|
|
||||||
Accept
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
</Card>
|
|
||||||
</Center>
|
|
||||||
</AppShell>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -9,11 +9,9 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Icon,
|
Icon,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
Box,
|
|
||||||
Tooltip,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { BsPlus, BsTrash } from "react-icons/bs";
|
import { BsTrash } from "react-icons/bs";
|
||||||
|
|
||||||
import AppShell from "~/components/nav/AppShell";
|
import AppShell from "~/components/nav/AppShell";
|
||||||
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
|
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
|
||||||
@@ -23,8 +21,6 @@ import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContent
|
|||||||
import CopiableCode from "~/components/CopiableCode";
|
import CopiableCode from "~/components/CopiableCode";
|
||||||
import { DeleteProjectDialog } from "~/components/projectSettings/DeleteProjectDialog";
|
import { DeleteProjectDialog } from "~/components/projectSettings/DeleteProjectDialog";
|
||||||
import AutoResizeTextArea from "~/components/AutoResizeTextArea";
|
import AutoResizeTextArea from "~/components/AutoResizeTextArea";
|
||||||
import MemberTable from "~/components/projectSettings/MemberTable";
|
|
||||||
import { InviteMemberModal } from "~/components/projectSettings/InviteMemberModal";
|
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
@@ -54,13 +50,12 @@ export default function Settings() {
|
|||||||
setName(selectedProject?.name);
|
setName(selectedProject?.name);
|
||||||
}, [selectedProject?.name]);
|
}, [selectedProject?.name]);
|
||||||
|
|
||||||
const inviteMemberModal = useDisclosure();
|
const deleteProjectOpen = useDisclosure();
|
||||||
const deleteProjectDialog = useDisclosure();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppShell requireAuth>
|
<AppShell>
|
||||||
<PageHeaderContainer px={{ base: 4, md: 8 }}>
|
<PageHeaderContainer>
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<ProjectBreadcrumbContents />
|
<ProjectBreadcrumbContents />
|
||||||
@@ -70,7 +65,7 @@ export default function Settings() {
|
|||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</PageHeaderContainer>
|
</PageHeaderContainer>
|
||||||
<VStack px={{ base: 4, md: 8 }} py={4} alignItems="flex-start" spacing={4}>
|
<VStack px={8} py={4} alignItems="flex-start" spacing={4}>
|
||||||
<VStack spacing={0} alignItems="flex-start">
|
<VStack spacing={0} alignItems="flex-start">
|
||||||
<Text fontSize="2xl" fontWeight="bold">
|
<Text fontSize="2xl" fontWeight="bold">
|
||||||
Project Settings
|
Project Settings
|
||||||
@@ -114,37 +109,6 @@ export default function Settings() {
|
|||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
<Divider backgroundColor="gray.300" />
|
<Divider backgroundColor="gray.300" />
|
||||||
<VStack w="full" alignItems="flex-start">
|
|
||||||
<Subtitle>Project Members</Subtitle>
|
|
||||||
|
|
||||||
<Text fontSize="sm">
|
|
||||||
Add members to your project to allow them to view and edit your project's data.
|
|
||||||
</Text>
|
|
||||||
<Box mt={4} w="full">
|
|
||||||
<MemberTable />
|
|
||||||
</Box>
|
|
||||||
<Tooltip
|
|
||||||
isDisabled={selectedProject?.role === "ADMIN"}
|
|
||||||
label="Only admins can invite new members"
|
|
||||||
hasArrow
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
colorScheme="orange"
|
|
||||||
borderRadius={4}
|
|
||||||
onClick={inviteMemberModal.onOpen}
|
|
||||||
mt={2}
|
|
||||||
_disabled={{
|
|
||||||
opacity: 0.6,
|
|
||||||
}}
|
|
||||||
isDisabled={selectedProject?.role !== "ADMIN"}
|
|
||||||
>
|
|
||||||
<Icon as={BsPlus} boxSize={5} />
|
|
||||||
<Text>Invite New Member</Text>
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</VStack>
|
|
||||||
<Divider backgroundColor="gray.300" />
|
|
||||||
<VStack alignItems="flex-start">
|
<VStack alignItems="flex-start">
|
||||||
<Subtitle>Project API Key</Subtitle>
|
<Subtitle>Project API Key</Subtitle>
|
||||||
<Text fontSize="sm">
|
<Text fontSize="sm">
|
||||||
@@ -177,7 +141,7 @@ export default function Settings() {
|
|||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
mt={2}
|
mt={2}
|
||||||
height="auto"
|
height="auto"
|
||||||
onClick={deleteProjectDialog.onOpen}
|
onClick={deleteProjectOpen.onOpen}
|
||||||
>
|
>
|
||||||
<Icon as={BsTrash} />
|
<Icon as={BsTrash} />
|
||||||
<Text overflowWrap="break-word" whiteSpace="normal" py={2}>
|
<Text overflowWrap="break-word" whiteSpace="normal" py={2}>
|
||||||
@@ -189,11 +153,7 @@ export default function Settings() {
|
|||||||
</VStack>
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
<InviteMemberModal isOpen={inviteMemberModal.isOpen} onClose={inviteMemberModal.onClose} />
|
<DeleteProjectDialog isOpen={deleteProjectOpen.isOpen} onClose={deleteProjectOpen.onClose} />
|
||||||
<DeleteProjectDialog
|
|
||||||
isOpen={deleteProjectDialog.isOpen}
|
|
||||||
onClose={deleteProjectDialog.onClose}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Text, VStack, Divider, HStack } from "@chakra-ui/react";
|
import { Text, VStack, Divider, HStack } from "@chakra-ui/react";
|
||||||
|
|
||||||
import AppShell from "~/components/nav/AppShell";
|
import AppShell from "~/components/nav/AppShell";
|
||||||
@@ -7,14 +6,9 @@ import LoggedCallsPaginator from "~/components/requestLogs/LoggedCallsPaginator"
|
|||||||
import ActionButton from "~/components/requestLogs/ActionButton";
|
import ActionButton from "~/components/requestLogs/ActionButton";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
import { RiFlaskLine } from "react-icons/ri";
|
import { RiFlaskLine } from "react-icons/ri";
|
||||||
import { FiFilter } from "react-icons/fi";
|
|
||||||
import LogFilters from "~/components/requestLogs/LogFilters/LogFilters";
|
|
||||||
|
|
||||||
export default function LoggedCalls() {
|
export default function LoggedCalls() {
|
||||||
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
|
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
|
||||||
|
|
||||||
const [filtersShown, setFiltersShown] = useState(true);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell title="Request Logs" requireAuth>
|
<AppShell title="Request Logs" requireAuth>
|
||||||
<VStack px={8} py={8} alignItems="flex-start" spacing={4} w="full">
|
<VStack px={8} py={8} alignItems="flex-start" spacing={4} w="full">
|
||||||
@@ -23,13 +17,6 @@ export default function LoggedCalls() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Divider />
|
<Divider />
|
||||||
<HStack w="full" justifyContent="flex-end">
|
<HStack w="full" justifyContent="flex-end">
|
||||||
<ActionButton
|
|
||||||
onClick={() => {
|
|
||||||
setFiltersShown(!filtersShown);
|
|
||||||
}}
|
|
||||||
label={filtersShown ? "Hide Filters" : "Show Filters"}
|
|
||||||
icon={FiFilter}
|
|
||||||
/>
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log("experimenting with these ids", selectedLogIds);
|
console.log("experimenting with these ids", selectedLogIds);
|
||||||
@@ -39,7 +26,6 @@ export default function LoggedCalls() {
|
|||||||
isDisabled={selectedLogIds.size === 0}
|
isDisabled={selectedLogIds.size === 0}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
{filtersShown && <LogFilters />}
|
|
||||||
<LoggedCallTable />
|
<LoggedCallTable />
|
||||||
<LoggedCallsPaginator />
|
<LoggedCallsPaginator />
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
95
app/src/server/api/external/openApiTrpc.ts
vendored
95
app/src/server/api/external/openApiTrpc.ts
vendored
@@ -1,95 +0,0 @@
|
|||||||
import type { ApiKey, Project } from "@prisma/client";
|
|
||||||
import { TRPCError, initTRPC } from "@trpc/server";
|
|
||||||
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
|
|
||||||
import superjson from "superjson";
|
|
||||||
import { type OpenApiMeta } from "trpc-openapi";
|
|
||||||
import { ZodError } from "zod";
|
|
||||||
import { prisma } from "~/server/db";
|
|
||||||
|
|
||||||
type CreateContextOptions = {
|
|
||||||
key:
|
|
||||||
| (ApiKey & {
|
|
||||||
project: Project;
|
|
||||||
})
|
|
||||||
| null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This helper generates the "internals" for a tRPC context. If you need to use it, you can export
|
|
||||||
* it from here.
|
|
||||||
*
|
|
||||||
* Examples of things you may need it for:
|
|
||||||
* - testing, so we don't have to mock Next.js' req/res
|
|
||||||
* - tRPC's `createSSGHelpers`, where we don't have req/res
|
|
||||||
*
|
|
||||||
* @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
|
|
||||||
*/
|
|
||||||
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
|
||||||
return {
|
|
||||||
key: opts.key,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createOpenApiContext = async (opts: CreateNextContextOptions) => {
|
|
||||||
const { req, res } = opts;
|
|
||||||
|
|
||||||
const apiKey = req.headers.authorization?.split(" ")[1] as string | null;
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
||||||
}
|
|
||||||
const key = await prisma.apiKey.findUnique({
|
|
||||||
where: { apiKey },
|
|
||||||
include: { project: true },
|
|
||||||
});
|
|
||||||
if (!key) {
|
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return createInnerTRPCContext({
|
|
||||||
key,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TRPCContext = Awaited<ReturnType<typeof createOpenApiContext>>;
|
|
||||||
|
|
||||||
const t = initTRPC
|
|
||||||
.context<typeof createOpenApiContext>()
|
|
||||||
.meta<OpenApiMeta>()
|
|
||||||
.create({
|
|
||||||
transformer: superjson,
|
|
||||||
errorFormatter({ shape, error }) {
|
|
||||||
return {
|
|
||||||
...shape,
|
|
||||||
data: {
|
|
||||||
...shape.data,
|
|
||||||
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createOpenApiRouter = t.router;
|
|
||||||
|
|
||||||
export const openApiPublicProc = t.procedure;
|
|
||||||
|
|
||||||
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
|
||||||
const enforceApiKey = t.middleware(async ({ ctx, next }) => {
|
|
||||||
if (!ctx.key) {
|
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return next({
|
|
||||||
ctx: { key: ctx.key },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Protected (authenticated) procedure
|
|
||||||
*
|
|
||||||
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
|
|
||||||
* the session is valid and guarantees `ctx.session.user` is not null.
|
|
||||||
*
|
|
||||||
* @see https://trpc.io/docs/procedures
|
|
||||||
*/
|
|
||||||
export const openApiProtectedProc = t.procedure.use(enforceApiKey);
|
|
||||||
@@ -8,11 +8,10 @@ import { evaluationsRouter } from "./routers/evaluations.router";
|
|||||||
import { worldChampsRouter } from "./routers/worldChamps.router";
|
import { worldChampsRouter } from "./routers/worldChamps.router";
|
||||||
import { datasetsRouter } from "./routers/datasets.router";
|
import { datasetsRouter } from "./routers/datasets.router";
|
||||||
import { datasetEntries } from "./routers/datasetEntries.router";
|
import { datasetEntries } from "./routers/datasetEntries.router";
|
||||||
|
import { externalApiRouter } from "./routers/externalApi.router";
|
||||||
import { projectsRouter } from "./routers/projects.router";
|
import { projectsRouter } from "./routers/projects.router";
|
||||||
import { dashboardRouter } from "./routers/dashboard.router";
|
import { dashboardRouter } from "./routers/dashboard.router";
|
||||||
import { loggedCallsRouter } from "./routers/loggedCalls.router";
|
import { loggedCallsRouter } from "./routers/loggedCalls.router";
|
||||||
import { usersRouter } from "./routers/users.router";
|
|
||||||
import { adminJobsRouter } from "./routers/adminJobs.router";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
@@ -32,8 +31,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
projects: projectsRouter,
|
projects: projectsRouter,
|
||||||
dashboard: dashboardRouter,
|
dashboard: dashboardRouter,
|
||||||
loggedCalls: loggedCallsRouter,
|
loggedCalls: loggedCallsRouter,
|
||||||
users: usersRouter,
|
externalApi: externalApiRouter,
|
||||||
adminJobs: adminJobsRouter,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
|
||||||
import { kysely } from "~/server/db";
|
|
||||||
import { requireIsAdmin } from "~/utils/accessControl";
|
|
||||||
|
|
||||||
export const adminJobsRouter = createTRPCRouter({
|
|
||||||
list: protectedProcedure.input(z.object({})).query(async ({ ctx }) => {
|
|
||||||
await requireIsAdmin(ctx);
|
|
||||||
|
|
||||||
return await kysely
|
|
||||||
.selectFrom("graphile_worker.jobs")
|
|
||||||
.limit(100)
|
|
||||||
.selectAll()
|
|
||||||
.orderBy("created_at", "desc")
|
|
||||||
.execute();
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -85,16 +85,15 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
return experimentsWithCounts;
|
return experimentsWithCounts;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
get: publicProcedure.input(z.object({ slug: z.string() })).query(async ({ input, ctx }) => {
|
get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
|
||||||
|
await requireCanViewExperiment(input.id, ctx);
|
||||||
const experiment = await prisma.experiment.findFirstOrThrow({
|
const experiment = await prisma.experiment.findFirstOrThrow({
|
||||||
where: { slug: input.slug },
|
where: { id: input.id },
|
||||||
include: {
|
include: {
|
||||||
project: true,
|
project: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await requireCanViewExperiment(experiment.id, ctx);
|
|
||||||
|
|
||||||
const canModify = ctx.session?.user.id
|
const canModify = ctx.session?.user.id
|
||||||
? await canModifyExperiment(experiment.id, ctx.session?.user.id)
|
? await canModifyExperiment(experiment.id, ctx.session?.user.id)
|
||||||
: false;
|
: false;
|
||||||
@@ -178,7 +177,6 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
existingToNewVariantIds.set(variant.id, newVariantId);
|
existingToNewVariantIds.set(variant.id, newVariantId);
|
||||||
variantsToCreate.push({
|
variantsToCreate.push({
|
||||||
...variant,
|
...variant,
|
||||||
uiId: uuidv4(),
|
|
||||||
id: newVariantId,
|
id: newVariantId,
|
||||||
experimentId: newExperimentId,
|
experimentId: newExperimentId,
|
||||||
});
|
});
|
||||||
@@ -192,7 +190,6 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
scenariosToCreate.push({
|
scenariosToCreate.push({
|
||||||
...scenario,
|
...scenario,
|
||||||
id: newScenarioId,
|
id: newScenarioId,
|
||||||
uiId: uuidv4(),
|
|
||||||
experimentId: newExperimentId,
|
experimentId: newExperimentId,
|
||||||
variableValues: scenario.variableValues as Prisma.InputJsonValue,
|
variableValues: scenario.variableValues as Prisma.InputJsonValue,
|
||||||
});
|
});
|
||||||
@@ -293,10 +290,7 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const newExperiment = await prisma.experiment.findUniqueOrThrow({
|
return newExperimentId;
|
||||||
where: { id: newExperimentId },
|
|
||||||
});
|
|
||||||
return newExperiment;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
@@ -341,6 +335,7 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo-0613",
|
model: "gpt-3.5-turbo-0613",
|
||||||
|
stream: true,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { type Prisma } from "@prisma/client";
|
|||||||
import { type JsonValue } from "type-fest";
|
import { type JsonValue } from "type-fest";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import { hashRequest } from "~/server/utils/hashObject";
|
import { hashRequest } from "~/server/utils/hashObject";
|
||||||
import modelProvider from "~/modelProviders/openai-ChatCompletion";
|
import modelProvider from "~/modelProviders/openai-ChatCompletion";
|
||||||
@@ -9,7 +12,6 @@ import {
|
|||||||
type ChatCompletion,
|
type ChatCompletion,
|
||||||
type CompletionCreateParams,
|
type CompletionCreateParams,
|
||||||
} from "openai/resources/chat/completions";
|
} from "openai/resources/chat/completions";
|
||||||
import { createOpenApiRouter, openApiProtectedProc } from "./openApiTrpc";
|
|
||||||
|
|
||||||
const reqValidator = z.object({
|
const reqValidator = z.object({
|
||||||
model: z.string(),
|
model: z.string(),
|
||||||
@@ -26,12 +28,12 @@ const respValidator = z.object({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const v1ApiRouter = createOpenApiRouter({
|
export const externalApiRouter = createTRPCRouter({
|
||||||
checkCache: openApiProtectedProc
|
checkCache: publicProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/check-cache",
|
path: "/v1/check-cache",
|
||||||
description: "Check if a prompt is cached",
|
description: "Check if a prompt is cached",
|
||||||
protect: true,
|
protect: true,
|
||||||
},
|
},
|
||||||
@@ -45,8 +47,7 @@ export const v1ApiRouter = createOpenApiRouter({
|
|||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
|
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
|
||||||
)
|
),
|
||||||
.default({}),
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(
|
.output(
|
||||||
@@ -55,8 +56,18 @@ export const v1ApiRouter = createOpenApiRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const apiKey = ctx.apiKey;
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
const key = await prisma.apiKey.findUnique({
|
||||||
|
where: { apiKey },
|
||||||
|
});
|
||||||
|
if (!key) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
const reqPayload = await reqValidator.spa(input.reqPayload);
|
const reqPayload = await reqValidator.spa(input.reqPayload);
|
||||||
const cacheKey = hashRequest(ctx.key.projectId, reqPayload as JsonValue);
|
const cacheKey = hashRequest(key.projectId, reqPayload as JsonValue);
|
||||||
|
|
||||||
const existingResponse = await prisma.loggedCallModelResponse.findFirst({
|
const existingResponse = await prisma.loggedCallModelResponse.findFirst({
|
||||||
where: { cacheKey },
|
where: { cacheKey },
|
||||||
@@ -66,26 +77,25 @@ export const v1ApiRouter = createOpenApiRouter({
|
|||||||
|
|
||||||
if (!existingResponse) return { respPayload: null };
|
if (!existingResponse) return { respPayload: null };
|
||||||
|
|
||||||
const newCall = await prisma.loggedCall.create({
|
await prisma.loggedCall.create({
|
||||||
data: {
|
data: {
|
||||||
projectId: ctx.key.projectId,
|
projectId: key.projectId,
|
||||||
requestedAt: new Date(input.requestedAt),
|
requestedAt: new Date(input.requestedAt),
|
||||||
cacheHit: true,
|
cacheHit: true,
|
||||||
modelResponseId: existingResponse.id,
|
modelResponseId: existingResponse.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await createTags(newCall.projectId, newCall.id, input.tags);
|
|
||||||
return {
|
return {
|
||||||
respPayload: existingResponse.respPayload,
|
respPayload: existingResponse.respPayload,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
report: openApiProtectedProc
|
report: publicProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/report",
|
path: "/v1/report",
|
||||||
description: "Report an API call",
|
description: "Report an API call",
|
||||||
protect: true,
|
protect: true,
|
||||||
},
|
},
|
||||||
@@ -103,16 +113,26 @@ export const v1ApiRouter = createOpenApiRouter({
|
|||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
|
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
|
||||||
)
|
),
|
||||||
.default({}),
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.object({ status: z.union([z.literal("ok"), z.literal("error")]) }))
|
.output(z.void())
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
console.log("GOT TAGS", input.tags);
|
||||||
|
const apiKey = ctx.apiKey;
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
const key = await prisma.apiKey.findUnique({
|
||||||
|
where: { apiKey },
|
||||||
|
});
|
||||||
|
if (!key) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
const reqPayload = await reqValidator.spa(input.reqPayload);
|
const reqPayload = await reqValidator.spa(input.reqPayload);
|
||||||
const respPayload = await respValidator.spa(input.respPayload);
|
const respPayload = await respValidator.spa(input.respPayload);
|
||||||
|
|
||||||
const requestHash = hashRequest(ctx.key.projectId, reqPayload as JsonValue);
|
const requestHash = hashRequest(key.projectId, reqPayload as JsonValue);
|
||||||
|
|
||||||
const newLoggedCallId = uuidv4();
|
const newLoggedCallId = uuidv4();
|
||||||
const newModelResponseId = uuidv4();
|
const newModelResponseId = uuidv4();
|
||||||
@@ -131,7 +151,7 @@ export const v1ApiRouter = createOpenApiRouter({
|
|||||||
prisma.loggedCall.create({
|
prisma.loggedCall.create({
|
||||||
data: {
|
data: {
|
||||||
id: newLoggedCallId,
|
id: newLoggedCallId,
|
||||||
projectId: ctx.key.projectId,
|
projectId: key.projectId,
|
||||||
requestedAt: new Date(input.requestedAt),
|
requestedAt: new Date(input.requestedAt),
|
||||||
cacheHit: false,
|
cacheHit: false,
|
||||||
model,
|
model,
|
||||||
@@ -165,79 +185,14 @@ export const v1ApiRouter = createOpenApiRouter({
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await createTags(ctx.key.projectId, newLoggedCallId, input.tags);
|
const tagsToCreate = Object.entries(input.tags ?? {}).map(([name, value]) => ({
|
||||||
return { status: "ok" };
|
loggedCallId: newLoggedCallId,
|
||||||
}),
|
// sanitize tags
|
||||||
localTestingOnlyGetLatestLoggedCall: openApiProtectedProc
|
name: name.replaceAll(/[^a-zA-Z0-9_]/g, "_"),
|
||||||
.meta({
|
value,
|
||||||
openapi: {
|
}));
|
||||||
method: "GET",
|
await prisma.loggedCallTag.createMany({
|
||||||
path: "/local-testing-only-get-latest-logged-call",
|
data: tagsToCreate,
|
||||||
description: "Get the latest logged call (only for local testing)",
|
|
||||||
protect: true, // Make sure to protect this endpoint
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.input(z.void())
|
|
||||||
.output(
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
createdAt: z.date(),
|
|
||||||
cacheHit: z.boolean(),
|
|
||||||
tags: z.record(z.string().nullable()),
|
|
||||||
modelResponse: z
|
|
||||||
.object({
|
|
||||||
id: z.string(),
|
|
||||||
statusCode: z.number().nullable(),
|
|
||||||
errorMessage: z.string().nullable(),
|
|
||||||
reqPayload: z.unknown(),
|
|
||||||
respPayload: z.unknown(),
|
|
||||||
})
|
|
||||||
.nullable(),
|
|
||||||
})
|
|
||||||
.nullable(),
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx }) => {
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
|
||||||
throw new Error("This operation is not allowed in production environment");
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestLoggedCall = await prisma.loggedCall.findFirst({
|
|
||||||
where: { projectId: ctx.key.projectId },
|
|
||||||
orderBy: { requestedAt: "desc" },
|
|
||||||
select: {
|
|
||||||
createdAt: true,
|
|
||||||
cacheHit: true,
|
|
||||||
tags: true,
|
|
||||||
id: true,
|
|
||||||
modelResponse: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
statusCode: true,
|
|
||||||
errorMessage: true,
|
|
||||||
reqPayload: true,
|
|
||||||
respPayload: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
|
||||||
latestLoggedCall && {
|
|
||||||
...latestLoggedCall,
|
|
||||||
tags: Object.fromEntries(latestLoggedCall.tags.map((tag) => [tag.name, tag.value])),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
async function createTags(projectId: string, loggedCallId: string, tags: Record<string, string>) {
|
|
||||||
const tagsToCreate = Object.entries(tags).map(([name, value]) => ({
|
|
||||||
projectId,
|
|
||||||
loggedCallId,
|
|
||||||
name: name.replaceAll(/[^a-zA-Z0-9_$.]/g, "_"),
|
|
||||||
value,
|
|
||||||
}));
|
|
||||||
await prisma.loggedCallTag.createMany({
|
|
||||||
data: tagsToCreate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,183 +1,33 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { type Expression, type SqlBool, sql, type RawBuilder } from "kysely";
|
|
||||||
import { jsonArrayFrom } from "kysely/helpers/postgres";
|
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
import { kysely, prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import { comparators, defaultFilterableFields } from "~/state/logFiltersSlice";
|
|
||||||
import { requireCanViewProject } from "~/utils/accessControl";
|
import { requireCanViewProject } from "~/utils/accessControl";
|
||||||
|
|
||||||
// create comparator type based off of comparators
|
|
||||||
const comparatorToSqlExpression = (comparator: (typeof comparators)[number], value: string) => {
|
|
||||||
return (reference: RawBuilder<unknown>): Expression<SqlBool> => {
|
|
||||||
switch (comparator) {
|
|
||||||
case "=":
|
|
||||||
return sql`${reference} = ${value}`;
|
|
||||||
case "!=":
|
|
||||||
// Handle NULL values
|
|
||||||
return sql`${reference} IS DISTINCT FROM ${value}`;
|
|
||||||
case "CONTAINS":
|
|
||||||
return sql`${reference} LIKE ${"%" + value + "%"}`;
|
|
||||||
case "NOT_CONTAINS":
|
|
||||||
return sql`(${reference} NOT LIKE ${"%" + value + "%"} OR ${reference} IS NULL)`;
|
|
||||||
default:
|
|
||||||
throw new Error("Unknown comparator");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loggedCallsRouter = createTRPCRouter({
|
export const loggedCallsRouter = createTRPCRouter({
|
||||||
list: protectedProcedure
|
list: protectedProcedure
|
||||||
.input(
|
.input(z.object({ projectId: z.string(), page: z.number(), pageSize: z.number() }))
|
||||||
z.object({
|
|
||||||
projectId: z.string(),
|
|
||||||
page: z.number(),
|
|
||||||
pageSize: z.number(),
|
|
||||||
filters: z.array(
|
|
||||||
z.object({
|
|
||||||
field: z.string(),
|
|
||||||
comparator: z.enum(comparators),
|
|
||||||
value: z.string(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const { projectId, page, pageSize } = input;
|
const { projectId, page, pageSize } = input;
|
||||||
|
|
||||||
await requireCanViewProject(projectId, ctx);
|
await requireCanViewProject(projectId, ctx);
|
||||||
|
|
||||||
const baseQuery = kysely
|
const calls = await prisma.loggedCall.findMany({
|
||||||
.selectFrom("LoggedCall as lc")
|
where: { projectId },
|
||||||
.leftJoin("LoggedCallModelResponse as lcmr", "lc.id", "lcmr.originalLoggedCallId")
|
orderBy: { requestedAt: "desc" },
|
||||||
.where((eb) => {
|
include: { tags: true, modelResponse: true },
|
||||||
const wheres: Expression<SqlBool>[] = [eb("lc.projectId", "=", projectId)];
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
for (const filter of input.filters) {
|
|
||||||
if (!filter.value) continue;
|
|
||||||
const filterExpression = comparatorToSqlExpression(filter.comparator, filter.value);
|
|
||||||
|
|
||||||
if (filter.field === "Request") {
|
|
||||||
wheres.push(filterExpression(sql.raw(`lcmr."reqPayload"::text`)));
|
|
||||||
}
|
|
||||||
if (filter.field === "Response") {
|
|
||||||
wheres.push(filterExpression(sql.raw(`lcmr."respPayload"::text`)));
|
|
||||||
}
|
|
||||||
if (filter.field === "Model") {
|
|
||||||
wheres.push(filterExpression(sql.raw(`lc."model"`)));
|
|
||||||
}
|
|
||||||
if (filter.field === "Status Code") {
|
|
||||||
wheres.push(filterExpression(sql.raw(`lcmr."statusCode"::text`)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return eb.and(wheres);
|
|
||||||
});
|
|
||||||
|
|
||||||
const tagFilters = input.filters.filter(
|
|
||||||
(filter) =>
|
|
||||||
!defaultFilterableFields.includes(
|
|
||||||
filter.field as (typeof defaultFilterableFields)[number],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let updatedBaseQuery = baseQuery;
|
|
||||||
|
|
||||||
for (let i = 0; i < tagFilters.length; i++) {
|
|
||||||
const filter = tagFilters[i];
|
|
||||||
if (!filter?.value) continue;
|
|
||||||
const tableAlias = `lct${i}`;
|
|
||||||
const filterExpression = comparatorToSqlExpression(filter.comparator, filter.value);
|
|
||||||
|
|
||||||
updatedBaseQuery = updatedBaseQuery
|
|
||||||
.leftJoin(`LoggedCallTag as ${tableAlias}`, (join) =>
|
|
||||||
join
|
|
||||||
.onRef("lc.id", "=", `${tableAlias}.loggedCallId`)
|
|
||||||
.on(`${tableAlias}.name`, "=", filter.field),
|
|
||||||
)
|
|
||||||
.where(filterExpression(sql.raw(`${tableAlias}.value`))) as unknown as typeof baseQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawCalls = await updatedBaseQuery
|
|
||||||
.select((eb) => [
|
|
||||||
"lc.id as id",
|
|
||||||
"lc.requestedAt as requestedAt",
|
|
||||||
"model",
|
|
||||||
"cacheHit",
|
|
||||||
"lc.requestedAt",
|
|
||||||
"receivedAt",
|
|
||||||
"reqPayload",
|
|
||||||
"respPayload",
|
|
||||||
"model",
|
|
||||||
"inputTokens",
|
|
||||||
"outputTokens",
|
|
||||||
"cost",
|
|
||||||
"statusCode",
|
|
||||||
"durationMs",
|
|
||||||
jsonArrayFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom("LoggedCallTag")
|
|
||||||
.select(["name", "value"])
|
|
||||||
.whereRef("loggedCallId", "=", "lc.id"),
|
|
||||||
).as("tags"),
|
|
||||||
])
|
|
||||||
.orderBy("lc.requestedAt", "desc")
|
|
||||||
.limit(pageSize)
|
|
||||||
.offset((page - 1) * pageSize)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
const calls = rawCalls.map((rawCall) => {
|
|
||||||
const tagsObject = rawCall.tags.reduce(
|
|
||||||
(acc, tag) => {
|
|
||||||
acc[tag.name] = tag.value;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, string | null>,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: rawCall.id,
|
|
||||||
requestedAt: rawCall.requestedAt,
|
|
||||||
model: rawCall.model,
|
|
||||||
cacheHit: rawCall.cacheHit,
|
|
||||||
modelResponse: {
|
|
||||||
receivedAt: rawCall.receivedAt,
|
|
||||||
reqPayload: rawCall.reqPayload,
|
|
||||||
respPayload: rawCall.respPayload,
|
|
||||||
inputTokens: rawCall.inputTokens,
|
|
||||||
outputTokens: rawCall.outputTokens,
|
|
||||||
cost: rawCall.cost,
|
|
||||||
statusCode: rawCall.statusCode,
|
|
||||||
durationMs: rawCall.durationMs,
|
|
||||||
},
|
|
||||||
tags: tagsObject,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const matchingLogIds = await updatedBaseQuery.select(["lc.id"]).execute();
|
const matchingLogs = await prisma.loggedCall.findMany({
|
||||||
|
where: { projectId },
|
||||||
const count = matchingLogIds.length;
|
select: { id: true },
|
||||||
|
|
||||||
return { calls, count, matchingLogIds: matchingLogIds.map((log) => log.id) };
|
|
||||||
}),
|
|
||||||
getTagNames: protectedProcedure
|
|
||||||
.input(z.object({ projectId: z.string() }))
|
|
||||||
.query(async ({ input, ctx }) => {
|
|
||||||
await requireCanViewProject(input.projectId, ctx);
|
|
||||||
|
|
||||||
const tags = await prisma.loggedCallTag.findMany({
|
|
||||||
distinct: ["name"],
|
|
||||||
where: {
|
|
||||||
projectId: input.projectId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
name: "asc",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return tags.map((tag) => tag.name);
|
const count = await prisma.loggedCall.count({
|
||||||
|
where: { projectId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { calls, count, matchingLogIds: matchingLogs.map((log) => log.id) };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,12 +51,6 @@ export const projectsRouter = createTRPCRouter({
|
|||||||
include: {
|
include: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
personalProjectUser: true,
|
personalProjectUser: true,
|
||||||
projectUsers: {
|
|
||||||
include: {
|
|
||||||
user: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
projectUserInvitations: true,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.projectUser.findFirst({
|
prisma.projectUser.findFirst({
|
||||||
@@ -64,7 +58,7 @@ export const projectsRouter = createTRPCRouter({
|
|||||||
userId: ctx.session.user.id,
|
userId: ctx.session.user.id,
|
||||||
projectId: input.id,
|
projectId: input.id,
|
||||||
role: {
|
role: {
|
||||||
in: ["ADMIN", "MEMBER", "VIEWER"],
|
in: ["ADMIN", "MEMBER"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -131,8 +131,6 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
const inputTokens = overallTokens._sum?.inputTokens ?? 0;
|
const inputTokens = overallTokens._sum?.inputTokens ?? 0;
|
||||||
const outputTokens = overallTokens._sum?.outputTokens ?? 0;
|
const outputTokens = overallTokens._sum?.outputTokens ?? 0;
|
||||||
|
|
||||||
const awaitingCompletions = outputCount < scenarioCount;
|
|
||||||
|
|
||||||
const awaitingEvals = !!evalResults.find(
|
const awaitingEvals = !!evalResults.find(
|
||||||
(result) => result.totalCount < scenarioCount * evals.length,
|
(result) => result.totalCount < scenarioCount * evals.length,
|
||||||
);
|
);
|
||||||
@@ -144,7 +142,6 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
overallCost: overallTokens._sum?.cost ?? 0,
|
overallCost: overallTokens._sum?.cost ?? 0,
|
||||||
scenarioCount,
|
scenarioCount,
|
||||||
outputCount,
|
outputCount,
|
||||||
awaitingCompletions,
|
|
||||||
awaitingEvals,
|
awaitingEvals,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import modelProviders from "~/modelProviders/modelProviders";
|
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import { queueQueryModel } from "~/server/tasks/queryModel.task";
|
import { queueQueryModel } from "~/server/tasks/queryModel.task";
|
||||||
@@ -61,7 +59,7 @@ export const scenarioVariantCellsRouter = createTRPCRouter({
|
|||||||
evalsComplete,
|
evalsComplete,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
hardRefetch: protectedProcedure
|
forceRefetch: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
scenarioId: z.string(),
|
scenarioId: z.string(),
|
||||||
@@ -85,10 +83,7 @@ export const scenarioVariantCellsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!cell) {
|
if (!cell) {
|
||||||
await generateNewCell(input.variantId, input.scenarioId, {
|
await generateNewCell(input.variantId, input.scenarioId, { stream: true });
|
||||||
stream: true,
|
|
||||||
hardRefetch: true,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,48 +94,6 @@ export const scenarioVariantCellsRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await queueQueryModel(cell.id, { stream: true, hardRefetch: true });
|
await queueQueryModel(cell.id, true);
|
||||||
}),
|
|
||||||
getTemplatedPromptMessage: publicProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
cellId: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
const cell = await prisma.scenarioVariantCell.findUnique({
|
|
||||||
where: { id: input.cellId },
|
|
||||||
include: {
|
|
||||||
promptVariant: true,
|
|
||||||
modelResponses: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!cell) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const promptMessages = (cell.prompt as { messages: [] })["messages"];
|
|
||||||
|
|
||||||
if (!promptMessages) return null;
|
|
||||||
|
|
||||||
const { modelProvider, model } = cell.promptVariant;
|
|
||||||
|
|
||||||
const provider = modelProviders[modelProvider as keyof typeof modelProviders];
|
|
||||||
|
|
||||||
if (!provider) return null;
|
|
||||||
|
|
||||||
const modelObj = provider.models[model as keyof typeof provider.models];
|
|
||||||
|
|
||||||
const templatePrompt = modelObj?.templatePrompt;
|
|
||||||
|
|
||||||
if (!templatePrompt) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
templatedPrompt: templatePrompt(promptMessages),
|
|
||||||
learnMoreUrl: modelObj.learnMoreUrl,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
|
||||||
import { prisma } from "~/server/db";
|
|
||||||
import { error, success } from "~/utils/errorHandling/standardResponses";
|
|
||||||
import { requireIsProjectAdmin, requireNothing } from "~/utils/accessControl";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { sendProjectInvitation } from "~/server/emails/sendProjectInvitation";
|
|
||||||
|
|
||||||
export const usersRouter = createTRPCRouter({
|
|
||||||
inviteToProject: protectedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
projectId: z.string(),
|
|
||||||
email: z.string().email(),
|
|
||||||
role: z.enum(["ADMIN", "MEMBER", "VIEWER"]),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
await requireIsProjectAdmin(input.projectId, ctx);
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
email: input.email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
const existingMembership = await prisma.projectUser.findUnique({
|
|
||||||
where: {
|
|
||||||
projectId_userId: {
|
|
||||||
projectId: input.projectId,
|
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingMembership) {
|
|
||||||
return error(`A user with ${input.email} is already a member of this project`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const invitation = await prisma.userInvitation.upsert({
|
|
||||||
where: {
|
|
||||||
projectId_email: {
|
|
||||||
projectId: input.projectId,
|
|
||||||
email: input.email,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
role: input.role,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
projectId: input.projectId,
|
|
||||||
email: input.email,
|
|
||||||
role: input.role,
|
|
||||||
invitationToken: uuidv4(),
|
|
||||||
senderId: ctx.session.user.id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
project: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await sendProjectInvitation({
|
|
||||||
invitationToken: invitation.invitationToken,
|
|
||||||
recipientEmail: input.email,
|
|
||||||
invitationSenderName: ctx.session.user.name || "",
|
|
||||||
invitationSenderEmail: ctx.session.user.email || "",
|
|
||||||
projectName: invitation.project.name,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// If we fail to send the email, we should delete the invitation
|
|
||||||
await prisma.userInvitation.delete({
|
|
||||||
where: {
|
|
||||||
invitationToken: invitation.invitationToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return error("Failed to send email");
|
|
||||||
}
|
|
||||||
|
|
||||||
return success();
|
|
||||||
}),
|
|
||||||
getProjectInvitation: protectedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
invitationToken: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.query(async ({ input, ctx }) => {
|
|
||||||
requireNothing(ctx);
|
|
||||||
|
|
||||||
const invitation = await prisma.userInvitation.findUnique({
|
|
||||||
where: {
|
|
||||||
invitationToken: input.invitationToken,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
project: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sender: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!invitation) {
|
|
||||||
throw new TRPCError({ code: "NOT_FOUND" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return invitation;
|
|
||||||
}),
|
|
||||||
acceptProjectInvitation: protectedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
invitationToken: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
requireNothing(ctx);
|
|
||||||
|
|
||||||
const invitation = await prisma.userInvitation.findUnique({
|
|
||||||
where: {
|
|
||||||
invitationToken: input.invitationToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!invitation) {
|
|
||||||
throw new TRPCError({ code: "NOT_FOUND" });
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.projectUser.create({
|
|
||||||
data: {
|
|
||||||
projectId: invitation.projectId,
|
|
||||||
userId: ctx.session.user.id,
|
|
||||||
role: invitation.role,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.userInvitation.delete({
|
|
||||||
where: {
|
|
||||||
invitationToken: input.invitationToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return success(invitation.projectId);
|
|
||||||
}),
|
|
||||||
cancelProjectInvitation: protectedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
invitationToken: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
requireNothing(ctx);
|
|
||||||
|
|
||||||
const invitation = await prisma.userInvitation.findUnique({
|
|
||||||
where: {
|
|
||||||
invitationToken: input.invitationToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!invitation) {
|
|
||||||
throw new TRPCError({ code: "NOT_FOUND" });
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.userInvitation.delete({
|
|
||||||
where: {
|
|
||||||
invitationToken: input.invitationToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return success();
|
|
||||||
}),
|
|
||||||
editProjectUserRole: protectedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
projectId: z.string(),
|
|
||||||
userId: z.string(),
|
|
||||||
role: z.enum(["ADMIN", "MEMBER", "VIEWER"]),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
await requireIsProjectAdmin(input.projectId, ctx);
|
|
||||||
|
|
||||||
await prisma.projectUser.update({
|
|
||||||
where: {
|
|
||||||
projectId_userId: {
|
|
||||||
projectId: input.projectId,
|
|
||||||
userId: input.userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
role: input.role,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return success();
|
|
||||||
}),
|
|
||||||
removeUserFromProject: protectedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
projectId: z.string(),
|
|
||||||
userId: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
await requireIsProjectAdmin(input.projectId, ctx);
|
|
||||||
|
|
||||||
await prisma.projectUser.delete({
|
|
||||||
where: {
|
|
||||||
projectId_userId: {
|
|
||||||
projectId: input.projectId,
|
|
||||||
userId: input.userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return success();
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -27,6 +27,7 @@ import { capturePath } from "~/utils/analytics/serverAnalytics";
|
|||||||
|
|
||||||
type CreateContextOptions = {
|
type CreateContextOptions = {
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
|
apiKey: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
@@ -45,6 +46,7 @@ const noOp = () => {};
|
|||||||
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||||
return {
|
return {
|
||||||
session: opts.session,
|
session: opts.session,
|
||||||
|
apiKey: opts.apiKey,
|
||||||
prisma,
|
prisma,
|
||||||
markAccessControlRun: noOp,
|
markAccessControlRun: noOp,
|
||||||
};
|
};
|
||||||
@@ -62,8 +64,11 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
|||||||
// Get the session from the server using the getServerSession wrapper function
|
// Get the session from the server using the getServerSession wrapper function
|
||||||
const session = await getServerAuthSession({ req, res });
|
const session = await getServerAuthSession({ req, res });
|
||||||
|
|
||||||
|
const apiKey = req.headers.authorization?.split(" ")[1] as string | null;
|
||||||
|
|
||||||
return createInnerTRPCContext({
|
return createInnerTRPCContext({
|
||||||
session,
|
session,
|
||||||
|
apiKey,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,27 @@
|
|||||||
import { type DB } from "./db.types";
|
import {
|
||||||
|
type Experiment,
|
||||||
import { PrismaClient } from "@prisma/client";
|
type PromptVariant,
|
||||||
|
type TestScenario,
|
||||||
|
type TemplateVariable,
|
||||||
|
type ScenarioVariantCell,
|
||||||
|
type ModelResponse,
|
||||||
|
type Evaluation,
|
||||||
|
type OutputEvaluation,
|
||||||
|
type Dataset,
|
||||||
|
type DatasetEntry,
|
||||||
|
type Project,
|
||||||
|
type ProjectUser,
|
||||||
|
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 { Kysely, PostgresDialect } from "kysely";
|
||||||
// TODO: Revert to normal import when our tsconfig.json is fixed
|
// TODO: Revert to normal import when our tsconfig.json is fixed
|
||||||
// import { Pool } from "pg";
|
// import { Pool } from "pg";
|
||||||
@@ -11,6 +32,30 @@ const Pool = (UntypedPool.default ? UntypedPool.default : UntypedPool) as typeof
|
|||||||
|
|
||||||
import { env } from "~/env.mjs";
|
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;
|
||||||
|
Project: Project;
|
||||||
|
ProjectUser: ProjectUser;
|
||||||
|
WorldChampEntrant: WorldChampEntrant;
|
||||||
|
LoggedCall: LoggedCall;
|
||||||
|
LoggedCallModelResponse: LoggedCallModelResponse;
|
||||||
|
LoggedCallTag: LoggedCallTag;
|
||||||
|
ApiKey: ApiKey;
|
||||||
|
Account: Account;
|
||||||
|
Session: Session;
|
||||||
|
User: User;
|
||||||
|
VerificationToken: VerificationToken;
|
||||||
|
}
|
||||||
|
|
||||||
const globalForPrisma = globalThis as unknown as {
|
const globalForPrisma = globalThis as unknown as {
|
||||||
prisma: PrismaClient | undefined;
|
prisma: PrismaClient | undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,336 +0,0 @@
|
|||||||
import type { ColumnType } from "kysely";
|
|
||||||
|
|
||||||
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
|
||||||
? ColumnType<S, I | undefined, U>
|
|
||||||
: ColumnType<T, T | undefined, T>;
|
|
||||||
|
|
||||||
export type Int8 = ColumnType<string, string | number | bigint, string | number | bigint>;
|
|
||||||
|
|
||||||
export type Json = ColumnType<JsonValue, string, string>;
|
|
||||||
|
|
||||||
export type JsonArray = JsonValue[];
|
|
||||||
|
|
||||||
export type JsonObject = {
|
|
||||||
[K in string]?: JsonValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type JsonPrimitive = boolean | null | number | string;
|
|
||||||
|
|
||||||
export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
|
|
||||||
|
|
||||||
export type Numeric = ColumnType<string, string | number, string | number>;
|
|
||||||
|
|
||||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
|
||||||
|
|
||||||
export interface _PrismaMigrations {
|
|
||||||
id: string;
|
|
||||||
checksum: string;
|
|
||||||
finished_at: Timestamp | null;
|
|
||||||
migration_name: string;
|
|
||||||
logs: string | null;
|
|
||||||
rolled_back_at: Timestamp | null;
|
|
||||||
started_at: Generated<Timestamp>;
|
|
||||||
applied_steps_count: Generated<number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Account {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
type: string;
|
|
||||||
provider: string;
|
|
||||||
providerAccountId: string;
|
|
||||||
refresh_token: string | null;
|
|
||||||
refresh_token_expires_in: number | null;
|
|
||||||
access_token: string | null;
|
|
||||||
expires_at: number | null;
|
|
||||||
token_type: string | null;
|
|
||||||
scope: string | null;
|
|
||||||
id_token: string | null;
|
|
||||||
session_state: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApiKey {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
apiKey: string;
|
|
||||||
projectId: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Dataset {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
projectId: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DatasetEntry {
|
|
||||||
id: string;
|
|
||||||
input: string;
|
|
||||||
output: string | null;
|
|
||||||
datasetId: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Evaluation {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
evalType: "CONTAINS" | "DOES_NOT_CONTAIN" | "GPT4_EVAL";
|
|
||||||
experimentId: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Experiment {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
sortIndex: Generated<number>;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
projectId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GraphileWorkerJobQueues {
|
|
||||||
queue_name: string;
|
|
||||||
job_count: number;
|
|
||||||
locked_at: Timestamp | null;
|
|
||||||
locked_by: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GraphileWorkerJobs {
|
|
||||||
id: Generated<Int8>;
|
|
||||||
queue_name: string | null;
|
|
||||||
task_identifier: string;
|
|
||||||
payload: Generated<Json>;
|
|
||||||
priority: Generated<number>;
|
|
||||||
run_at: Generated<Timestamp>;
|
|
||||||
attempts: Generated<number>;
|
|
||||||
max_attempts: Generated<number>;
|
|
||||||
last_error: string | null;
|
|
||||||
created_at: Generated<Timestamp>;
|
|
||||||
updated_at: Generated<Timestamp>;
|
|
||||||
key: string | null;
|
|
||||||
locked_at: Timestamp | null;
|
|
||||||
locked_by: string | null;
|
|
||||||
revision: Generated<number>;
|
|
||||||
flags: Json | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GraphileWorkerKnownCrontabs {
|
|
||||||
identifier: string;
|
|
||||||
known_since: Timestamp;
|
|
||||||
last_execution: Timestamp | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GraphileWorkerMigrations {
|
|
||||||
id: number;
|
|
||||||
ts: Generated<Timestamp>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoggedCall {
|
|
||||||
id: string;
|
|
||||||
requestedAt: Timestamp;
|
|
||||||
cacheHit: boolean;
|
|
||||||
modelResponseId: string | null;
|
|
||||||
projectId: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
model: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoggedCallModelResponse {
|
|
||||||
id: string;
|
|
||||||
reqPayload: Json;
|
|
||||||
statusCode: number | null;
|
|
||||||
respPayload: Json | null;
|
|
||||||
errorMessage: string | null;
|
|
||||||
requestedAt: Timestamp;
|
|
||||||
receivedAt: Timestamp;
|
|
||||||
cacheKey: string | null;
|
|
||||||
durationMs: number | null;
|
|
||||||
inputTokens: number | null;
|
|
||||||
outputTokens: number | null;
|
|
||||||
finishReason: string | null;
|
|
||||||
completionId: string | null;
|
|
||||||
cost: Numeric | null;
|
|
||||||
originalLoggedCallId: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoggedCallTag {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
value: string | null;
|
|
||||||
loggedCallId: string;
|
|
||||||
projectId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelResponse {
|
|
||||||
id: string;
|
|
||||||
cacheKey: string;
|
|
||||||
respPayload: Json | null;
|
|
||||||
inputTokens: number | null;
|
|
||||||
outputTokens: number | null;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
scenarioVariantCellId: string;
|
|
||||||
cost: number | null;
|
|
||||||
requestedAt: Timestamp | null;
|
|
||||||
receivedAt: Timestamp | null;
|
|
||||||
statusCode: number | null;
|
|
||||||
errorMessage: string | null;
|
|
||||||
retryTime: Timestamp | null;
|
|
||||||
outdated: Generated<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OutputEvaluation {
|
|
||||||
id: string;
|
|
||||||
result: number;
|
|
||||||
details: string | null;
|
|
||||||
modelResponseId: string;
|
|
||||||
evaluationId: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Project {
|
|
||||||
id: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
personalProjectUserId: string | null;
|
|
||||||
name: Generated<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProjectUser {
|
|
||||||
id: string;
|
|
||||||
role: "ADMIN" | "MEMBER" | "VIEWER";
|
|
||||||
projectId: string;
|
|
||||||
userId: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PromptVariant {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
uiId: string;
|
|
||||||
visible: Generated<boolean>;
|
|
||||||
sortIndex: Generated<number>;
|
|
||||||
experimentId: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
promptConstructor: string;
|
|
||||||
model: string;
|
|
||||||
promptConstructorVersion: number;
|
|
||||||
modelProvider: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScenarioVariantCell {
|
|
||||||
id: string;
|
|
||||||
errorMessage: string | null;
|
|
||||||
promptVariantId: string;
|
|
||||||
testScenarioId: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
retrievalStatus: Generated<"COMPLETE" | "ERROR" | "IN_PROGRESS" | "PENDING">;
|
|
||||||
prompt: Json | null;
|
|
||||||
jobQueuedAt: Timestamp | null;
|
|
||||||
jobStartedAt: Timestamp | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Session {
|
|
||||||
id: string;
|
|
||||||
sessionToken: string;
|
|
||||||
userId: string;
|
|
||||||
expires: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TemplateVariable {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
experimentId: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestScenario {
|
|
||||||
id: string;
|
|
||||||
variableValues: Json;
|
|
||||||
uiId: string;
|
|
||||||
visible: Generated<boolean>;
|
|
||||||
sortIndex: Generated<number>;
|
|
||||||
experimentId: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
email: string | null;
|
|
||||||
emailVerified: Timestamp | null;
|
|
||||||
image: string | null;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Generated<Timestamp>;
|
|
||||||
role: Generated<"ADMIN" | "USER">;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserInvitation {
|
|
||||||
id: string;
|
|
||||||
projectId: string;
|
|
||||||
email: string;
|
|
||||||
role: "ADMIN" | "MEMBER" | "VIEWER";
|
|
||||||
invitationToken: string;
|
|
||||||
senderId: string;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VerificationToken {
|
|
||||||
identifier: string;
|
|
||||||
token: string;
|
|
||||||
expires: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorldChampEntrant {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
approved: Generated<boolean>;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
updatedAt: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DB {
|
|
||||||
_prisma_migrations: _PrismaMigrations;
|
|
||||||
Account: Account;
|
|
||||||
ApiKey: ApiKey;
|
|
||||||
Dataset: Dataset;
|
|
||||||
DatasetEntry: DatasetEntry;
|
|
||||||
Evaluation: Evaluation;
|
|
||||||
Experiment: Experiment;
|
|
||||||
"graphile_worker.job_queues": GraphileWorkerJobQueues;
|
|
||||||
"graphile_worker.jobs": GraphileWorkerJobs;
|
|
||||||
"graphile_worker.known_crontabs": GraphileWorkerKnownCrontabs;
|
|
||||||
"graphile_worker.migrations": GraphileWorkerMigrations;
|
|
||||||
LoggedCall: LoggedCall;
|
|
||||||
LoggedCallModelResponse: LoggedCallModelResponse;
|
|
||||||
LoggedCallTag: LoggedCallTag;
|
|
||||||
ModelResponse: ModelResponse;
|
|
||||||
OutputEvaluation: OutputEvaluation;
|
|
||||||
Project: Project;
|
|
||||||
ProjectUser: ProjectUser;
|
|
||||||
PromptVariant: PromptVariant;
|
|
||||||
ScenarioVariantCell: ScenarioVariantCell;
|
|
||||||
Session: Session;
|
|
||||||
TemplateVariable: TemplateVariable;
|
|
||||||
TestScenario: TestScenario;
|
|
||||||
User: User;
|
|
||||||
UserInvitation: UserInvitation;
|
|
||||||
VerificationToken: VerificationToken;
|
|
||||||
WorldChampEntrant: WorldChampEntrant;
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { marked } from "marked";
|
|
||||||
import nodemailer from "nodemailer";
|
|
||||||
import { env } from "~/env.mjs";
|
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
// All the SMTP_ env vars come from https://app.brevo.com/settings/keys/smtp
|
|
||||||
// @ts-expect-error nodemailer types are wrong
|
|
||||||
host: env.SMTP_HOST,
|
|
||||||
port: env.SMTP_PORT,
|
|
||||||
auth: {
|
|
||||||
user: env.SMTP_LOGIN,
|
|
||||||
pass: env.SMTP_PASSWORD,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const sendEmail = async (options: { to: string; subject: string; body: string }) => {
|
|
||||||
const bodyHtml = await marked.parseInline(options.body, { mangle: false });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await transporter.sendMail({
|
|
||||||
from: env.SENDER_EMAIL,
|
|
||||||
to: options.to,
|
|
||||||
subject: options.subject,
|
|
||||||
html: bodyHtml,
|
|
||||||
text: options.body,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("error sending email", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { env } from "~/env.mjs";
|
|
||||||
import { sendEmail } from "./sendEmail";
|
|
||||||
|
|
||||||
export const sendProjectInvitation = async ({
|
|
||||||
invitationToken,
|
|
||||||
recipientEmail,
|
|
||||||
invitationSenderName,
|
|
||||||
invitationSenderEmail,
|
|
||||||
projectName,
|
|
||||||
}: {
|
|
||||||
invitationToken: string;
|
|
||||||
recipientEmail: string;
|
|
||||||
invitationSenderName: string;
|
|
||||||
invitationSenderEmail: string;
|
|
||||||
projectName: string;
|
|
||||||
}) => {
|
|
||||||
const invitationLink = `${env.NEXT_PUBLIC_HOST}/invitations/${invitationToken}`;
|
|
||||||
|
|
||||||
const emailBody = `
|
|
||||||
<p>You have been invited to join ${projectName} by ${invitationSenderName} (${invitationSenderEmail}).</p>
|
|
||||||
<p>Click <a href="${invitationLink}">here</a> to accept the invitation.</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
await sendEmail({
|
|
||||||
to: recipientEmail,
|
|
||||||
subject: "You've been invited to join a project",
|
|
||||||
body: emailBody,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import { openApiDocument } from "~/pages/api/v1/openapi.json";
|
import { openApiDocument } from "~/pages/api/openapi.json";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import { generate } from "openapi-typescript-codegen";
|
|
||||||
|
|
||||||
const scriptPath = import.meta.url.replace("file://", "");
|
const scriptPath = import.meta.url.replace("file://", "");
|
||||||
const clientLibsPath = path.join(path.dirname(scriptPath), "../../../../client-libs");
|
const clientLibsPath = path.join(path.dirname(scriptPath), "../../../../client-libs");
|
||||||
@@ -19,20 +18,13 @@ console.log("Generating TypeScript client");
|
|||||||
const tsClientPath = path.join(clientLibsPath, "typescript/src/codegen");
|
const tsClientPath = path.join(clientLibsPath, "typescript/src/codegen");
|
||||||
|
|
||||||
fs.rmSync(tsClientPath, { recursive: true, force: true });
|
fs.rmSync(tsClientPath, { recursive: true, force: true });
|
||||||
fs.mkdirSync(tsClientPath, { recursive: true });
|
|
||||||
|
|
||||||
await generate({
|
execSync(
|
||||||
input: openApiDocument,
|
`pnpm dlx @openapitools/openapi-generator-cli generate -i "${schemaPath}" -g typescript-axios -o "${tsClientPath}"`,
|
||||||
output: tsClientPath,
|
{
|
||||||
clientName: "OPClient",
|
stdio: "inherit",
|
||||||
httpClient: "node",
|
},
|
||||||
});
|
);
|
||||||
// execSync(
|
|
||||||
// `pnpm run openapi generate --input "${schemaPath}" --output "${tsClientPath}" --name OPClient --client node`,
|
|
||||||
// {
|
|
||||||
// stdio: "inherit",
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
|
|
||||||
console.log("Generating Python client");
|
console.log("Generating Python client");
|
||||||
|
|
||||||
|
|||||||
19
app/src/server/scripts/openai-test.ts
Normal file
19
app/src/server/scripts/openai-test.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import "dotenv/config";
|
||||||
|
import { openai } from "../utils/openai";
|
||||||
|
|
||||||
|
const resp = await openai.chat.completions.create({
|
||||||
|
model: "gpt-3.5-turbo-0613",
|
||||||
|
stream: true,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "count to 20",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const part of resp) {
|
||||||
|
console.log("part", part);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("final resp", resp);
|
||||||
@@ -1,26 +1,15 @@
|
|||||||
import { type Helpers, type Task, makeWorkerUtils, TaskSpec } from "graphile-worker";
|
// Import necessary dependencies
|
||||||
|
import { quickAddJob, type Helpers, type Task } from "graphile-worker";
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
let workerUtilsPromise: ReturnType<typeof makeWorkerUtils> | null = null;
|
// Define the defineTask function
|
||||||
|
|
||||||
function workerUtils() {
|
|
||||||
if (!workerUtilsPromise) {
|
|
||||||
workerUtilsPromise = makeWorkerUtils({
|
|
||||||
connectionString: env.DATABASE_URL,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return workerUtilsPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
function defineTask<TPayload>(
|
function defineTask<TPayload>(
|
||||||
taskIdentifier: string,
|
taskIdentifier: string,
|
||||||
taskHandler: (payload: TPayload, helpers: Helpers) => Promise<void>,
|
taskHandler: (payload: TPayload, helpers: Helpers) => Promise<void>,
|
||||||
) {
|
) {
|
||||||
const enqueue = async (payload: TPayload, spec?: TaskSpec) => {
|
const enqueue = async (payload: TPayload, runAt?: Date) => {
|
||||||
console.log("Enqueuing task", taskIdentifier, payload);
|
console.log("Enqueuing task", taskIdentifier, payload);
|
||||||
|
await quickAddJob({ connectionString: env.DATABASE_URL }, taskIdentifier, payload, { runAt });
|
||||||
const utils = await workerUtils();
|
|
||||||
return await utils.addJob(taskIdentifier, payload, spec);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = (payload: TPayload, helpers: Helpers) => {
|
const handler = (payload: TPayload, helpers: Helpers) => {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ function calculateDelay(numPreviousTries: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const queryModel = defineTask<QueryModelJob>("queryModel", async (task) => {
|
export const queryModel = defineTask<QueryModelJob>("queryModel", async (task) => {
|
||||||
|
console.log("RUNNING TASK", task);
|
||||||
const { cellId, stream, numPreviousTries } = task;
|
const { cellId, stream, numPreviousTries } = task;
|
||||||
const cell = await prisma.scenarioVariantCell.findUnique({
|
const cell = await prisma.scenarioVariantCell.findUnique({
|
||||||
where: { id: cellId },
|
where: { id: cellId },
|
||||||
@@ -152,7 +153,7 @@ export const queryModel = defineTask<QueryModelJob>("queryModel", async (task) =
|
|||||||
stream,
|
stream,
|
||||||
numPreviousTries: numPreviousTries + 1,
|
numPreviousTries: numPreviousTries + 1,
|
||||||
},
|
},
|
||||||
{ runAt: retryTime, jobKey: cellId, priority: 3 },
|
retryTime,
|
||||||
);
|
);
|
||||||
await prisma.scenarioVariantCell.update({
|
await prisma.scenarioVariantCell.update({
|
||||||
where: { id: cellId },
|
where: { id: cellId },
|
||||||
@@ -171,13 +172,7 @@ export const queryModel = defineTask<QueryModelJob>("queryModel", async (task) =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queueQueryModel = async (
|
export const queueQueryModel = async (cellId: string, stream: boolean) => {
|
||||||
cellId: string,
|
|
||||||
options: { stream?: boolean; hardRefetch?: boolean } = {},
|
|
||||||
) => {
|
|
||||||
// Hard refetches are higher priority than streamed queries, which are higher priority than non-streamed queries.
|
|
||||||
const jobPriority = options.hardRefetch ? 0 : options.stream ? 1 : 2;
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
prisma.scenarioVariantCell.update({
|
prisma.scenarioVariantCell.update({
|
||||||
where: {
|
where: {
|
||||||
@@ -189,13 +184,6 @@ export const queueQueryModel = async (
|
|||||||
jobQueuedAt: new Date(),
|
jobQueuedAt: new Date(),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
queryModel.enqueue({ cellId, stream, numPreviousTries: 0 }),
|
||||||
queryModel.enqueue(
|
|
||||||
{ cellId, stream: options.stream ?? false, numPreviousTries: 0 },
|
|
||||||
|
|
||||||
// Streamed queries are higher priority than non-streamed queries. Lower
|
|
||||||
// numbers are higher priority in graphile-worker.
|
|
||||||
{ jobKey: cellId, priority: jobPriority },
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,5 @@ export const runNewEval = defineTask<RunNewEvalJob>("runNewEval", async (task) =
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const queueRunNewEval = async (experimentId: string) => {
|
export const queueRunNewEval = async (experimentId: string) => {
|
||||||
// Evals are lower priority than completions
|
await runNewEval.enqueue({ experimentId });
|
||||||
await runNewEval.enqueue({ experimentId }, { priority: 4 });
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const taskList = registeredTasks.reduce((acc, task) => {
|
|||||||
// Run a worker to execute jobs:
|
// Run a worker to execute jobs:
|
||||||
const runner = await run({
|
const runner = await run({
|
||||||
connectionString: env.DATABASE_URL,
|
connectionString: env.DATABASE_URL,
|
||||||
concurrency: env.WORKER_CONCURRENCY,
|
concurrency: 10,
|
||||||
// Install signal handlers for graceful shutdown on SIGINT, SIGTERM, etc
|
// Install signal handlers for graceful shutdown on SIGINT, SIGTERM, etc
|
||||||
noHandleSignals: false,
|
noHandleSignals: false,
|
||||||
pollInterval: 1000,
|
pollInterval: 1000,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const requestUpdatedPromptFunction = async (
|
|||||||
originalModelProvider.inputSchema,
|
originalModelProvider.inputSchema,
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
)}`,
|
)}\n\nDo not add any assistant messages.`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ import cryptoRandomString from "crypto-random-string";
|
|||||||
|
|
||||||
const KEY_LENGTH = 42;
|
const KEY_LENGTH = 42;
|
||||||
|
|
||||||
export const generateApiKey = () => `opk_${cryptoRandomString({ length: KEY_LENGTH })}`;
|
export const generateApiKey = () => `opc_${cryptoRandomString({ length: KEY_LENGTH })}`;
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import parsePromptConstructor from "~/promptConstructor/parse";
|
|||||||
export const generateNewCell = async (
|
export const generateNewCell = async (
|
||||||
variantId: string,
|
variantId: string,
|
||||||
scenarioId: string,
|
scenarioId: string,
|
||||||
options: { stream?: boolean; hardRefetch?: boolean } = {},
|
options?: { stream?: boolean },
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
|
const stream = options?.stream ?? false;
|
||||||
|
|
||||||
const variant = await prisma.promptVariant.findUnique({
|
const variant = await prisma.promptVariant.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: variantId,
|
id: variantId,
|
||||||
@@ -119,6 +121,6 @@ export const generateNewCell = async (
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await queueQueryModel(cell.id, options);
|
await queueQueryModel(cell.id, stream);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { type ClientOptions } from "openai";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import OpenAI, { type ClientOptions } from "openpipe/src/openai";
|
import OpenAI from "openpipe/src/openai";
|
||||||
|
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
@@ -15,13 +16,7 @@ try {
|
|||||||
config = JSON.parse(jsonData.toString());
|
config = JSON.parse(jsonData.toString());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Set a dummy key so it doesn't fail at build time
|
// Set a dummy key so it doesn't fail at build time
|
||||||
config = {
|
config = { apiKey: env.OPENAI_API_KEY ?? "dummy-key" };
|
||||||
apiKey: env.OPENAI_API_KEY ?? "dummy-key",
|
|
||||||
openpipe: {
|
|
||||||
apiKey: env.OPENPIPE_API_KEY,
|
|
||||||
baseUrl: "http://localhost:3000/api/v1",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// export const openai = env.OPENPIPE_API_KEY ? new OpenAI.OpenAI(config) : new OriginalOpenAI(config);
|
// export const openai = env.OPENPIPE_API_KEY ? new OpenAI.OpenAI(config) : new OriginalOpenAI(config);
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import { type SliceCreator } from "./store";
|
|
||||||
|
|
||||||
export const comparators = ["=", "!=", "CONTAINS", "NOT_CONTAINS"] as const;
|
|
||||||
|
|
||||||
export const defaultFilterableFields = ["Request", "Response", "Model", "Status Code"] as const;
|
|
||||||
|
|
||||||
export interface LogFilter {
|
|
||||||
id: string;
|
|
||||||
field: string;
|
|
||||||
comparator: (typeof comparators)[number];
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LogFiltersSlice = {
|
|
||||||
filters: LogFilter[];
|
|
||||||
addFilter: (filter: LogFilter) => void;
|
|
||||||
updateFilter: (filter: LogFilter) => void;
|
|
||||||
deleteFilter: (id: string) => void;
|
|
||||||
clearSelectedLogIds: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createLogFiltersSlice: SliceCreator<LogFiltersSlice> = (set, get) => ({
|
|
||||||
filters: [],
|
|
||||||
addFilter: (filter: LogFilter) =>
|
|
||||||
set((state) => {
|
|
||||||
state.logFilters.filters.push(filter);
|
|
||||||
}),
|
|
||||||
updateFilter: (filter: LogFilter) =>
|
|
||||||
set((state) => {
|
|
||||||
const index = state.logFilters.filters.findIndex((f) => f.id === filter.id);
|
|
||||||
state.logFilters.filters[index] = filter;
|
|
||||||
}),
|
|
||||||
deleteFilter: (id: string) =>
|
|
||||||
set((state) => {
|
|
||||||
const index = state.logFilters.filters.findIndex((f) => f.id === id);
|
|
||||||
state.logFilters.filters.splice(index, 1);
|
|
||||||
}),
|
|
||||||
clearSelectedLogIds: () =>
|
|
||||||
set((state) => {
|
|
||||||
state.logFilters.filters = [];
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { type SliceCreator } from "./store";
|
import { type SliceCreator } from "./store";
|
||||||
|
|
||||||
|
export const editorBackground = "#fafafa";
|
||||||
|
|
||||||
export type SelectedLogsSlice = {
|
export type SelectedLogsSlice = {
|
||||||
selectedLogIds: Set<string>;
|
selectedLogIds: Set<string>;
|
||||||
toggleSelectedLogId: (id: string) => void;
|
toggleSelectedLogId: (id: string) => void;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import { type APIClient } from "~/utils/api";
|
import { type APIClient } from "~/utils/api";
|
||||||
import { persistOptions, type stateToPersist } from "./persist";
|
import { persistOptions, type stateToPersist } from "./persist";
|
||||||
import { type SelectedLogsSlice, createSelectedLogsSlice } from "./selectedLogsSlice";
|
import { type SelectedLogsSlice, createSelectedLogsSlice } from "./selectedLogsSlice";
|
||||||
import { type LogFiltersSlice, createLogFiltersSlice } from "./logFiltersSlice";
|
|
||||||
|
|
||||||
enableMapSet();
|
enableMapSet();
|
||||||
|
|
||||||
@@ -24,7 +23,6 @@ export type State = {
|
|||||||
selectedProjectId: string | null;
|
selectedProjectId: string | null;
|
||||||
setSelectedProjectId: (id: string) => void;
|
setSelectedProjectId: (id: string) => void;
|
||||||
selectedLogs: SelectedLogsSlice;
|
selectedLogs: SelectedLogsSlice;
|
||||||
logFilters: LogFiltersSlice;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SliceCreator<T> = StateCreator<State, [["zustand/immer", never]], [], T>;
|
export type SliceCreator<T> = StateCreator<State, [["zustand/immer", never]], [], T>;
|
||||||
@@ -60,7 +58,6 @@ const useBaseStore = create<
|
|||||||
state.selectedProjectId = id;
|
state.selectedProjectId = id;
|
||||||
}),
|
}),
|
||||||
selectedLogs: createSelectedLogsSlice(set, get, ...rest),
|
selectedLogs: createSelectedLogsSlice(set, get, ...rest),
|
||||||
logFilters: createLogFiltersSlice(set, get, ...rest),
|
|
||||||
})),
|
})),
|
||||||
persistOptions,
|
persistOptions,
|
||||||
),
|
),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user