Compare commits
97 Commits
save-butto
...
add-lib
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11985a0dcc | ||
|
|
0a0c5c5dda | ||
|
|
955716eb77 | ||
|
|
2c65ad0c8f | ||
|
|
26b6fa4f0c | ||
|
|
807665fdc1 | ||
|
|
d6597d2c8a | ||
|
|
566d67bf48 | ||
|
|
d4fb8b689a | ||
|
|
98b231c8bd | ||
|
|
45afb1f1f4 | ||
|
|
2bffb03766 | ||
|
|
223b990005 | ||
|
|
fa61c9c472 | ||
|
|
1309a6ec5d | ||
|
|
17a6fd31a5 | ||
|
|
e1cbeccb90 | ||
|
|
d6b97b29f7 | ||
|
|
09140f8b5f | ||
|
|
9952dd93d8 | ||
|
|
e0b457c6c5 | ||
|
|
0c37506975 | ||
|
|
2b2e0ab8ee | ||
|
|
3dbb06ec00 | ||
|
|
85d42a014b | ||
|
|
7d1ded3b18 | ||
|
|
b00f6dd04b | ||
|
|
2e395e4d39 | ||
|
|
4b06d05908 | ||
|
|
aabf355b81 | ||
|
|
61e5f0775d | ||
|
|
cc1d1178da | ||
|
|
7466db63df | ||
|
|
79a0b03bf8 | ||
|
|
6fb7a82d72 | ||
|
|
4ea30a3ba3 | ||
|
|
52d1d5c7ee | ||
|
|
46036a44d2 | ||
|
|
3753fe5c16 | ||
|
|
213a00a8e6 | ||
|
|
af9943eefc | ||
|
|
741128e0f4 | ||
|
|
aff14539d8 | ||
|
|
1af81a50a9 | ||
|
|
7e1fbb3767 | ||
|
|
a5d972005e | ||
|
|
a180b5bef2 | ||
|
|
55c697223e | ||
|
|
9978075867 | ||
|
|
847753c32b | ||
|
|
372c2512c9 | ||
|
|
332a2101c0 | ||
|
|
1822fe198e | ||
|
|
f06e1db3db | ||
|
|
ded6678e97 | ||
|
|
9314a86857 | ||
|
|
54dcb4a567 | ||
|
|
2c8c8d07cf | ||
|
|
e885bdd365 | ||
|
|
86dc36a656 | ||
|
|
55c077d604 | ||
|
|
e598e454d0 | ||
|
|
6e3f90cd2f | ||
|
|
eec894e101 | ||
|
|
f797fc3fa4 | ||
|
|
335dc0357f | ||
|
|
e6e2c706c2 | ||
|
|
7d2166b305 | ||
|
|
60765e51ac | ||
|
|
2c4ba6eb9b | ||
|
|
4c97b9f147 | ||
|
|
58892d8b63 | ||
|
|
4fa2dffbcb | ||
|
|
654f8c7cf2 | ||
|
|
d02482468d | ||
|
|
5c6ed22f1d | ||
|
|
2cb623f332 | ||
|
|
1c1cefe286 | ||
|
|
b4aa95edca | ||
|
|
1dcdba04a6 | ||
|
|
e0e64c4207 | ||
|
|
fa5b1ab1c5 | ||
|
|
999a4c08fa | ||
|
|
374d0237ee | ||
|
|
b1f873623d | ||
|
|
4131aa67d0 | ||
|
|
8e7a6d3ae2 | ||
|
|
7d41e94ca2 | ||
|
|
011b12abb9 | ||
|
|
1ba18015bc | ||
|
|
54369dba54 | ||
|
|
6b84a59372 | ||
|
|
8db8aeacd3 | ||
|
|
64bd71e370 | ||
|
|
ca21a7af06 | ||
|
|
3b99b7bd2b | ||
|
|
0c3bdbe4f2 |
2
.github/workflows/ci.yaml
vendored
@@ -3,6 +3,8 @@ name: CI checks
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
run-checks:
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
src/codegen/openai.schema.json
|
||||
pnpm-lock.yaml
|
||||
5
.vscode/settings.json
vendored
@@ -1,6 +1,3 @@
|
||||
{
|
||||
"eslint.format.enable": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
"eslint.format.enable": true
|
||||
}
|
||||
|
||||
@@ -17,4 +17,15 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/openpipe?schema=publ
|
||||
# https://help.openai.com/en/articles/4936850-where-do-i-find-my-secret-api-key
|
||||
OPENAI_API_KEY=""
|
||||
|
||||
# Replicate API token. Create a token here: https://replicate.com/account/api-tokens
|
||||
REPLICATE_API_TOKEN=""
|
||||
|
||||
NEXT_PUBLIC_SOCKET_URL="http://localhost:3318"
|
||||
|
||||
# Next Auth
|
||||
NEXTAUTH_SECRET="your_secret"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# Next Auth Github Provider
|
||||
GITHUB_CLIENT_ID="your_client_id"
|
||||
GITHUB_CLIENT_SECRET="your_secret"
|
||||
26
.gitignore → app/.gitignore
vendored
@@ -1,9 +1,11 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# App files
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
/.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
@@ -15,28 +17,28 @@
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
/next-env.d.ts
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
/.DS_Store
|
||||
/*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
/npm-debug.log*
|
||||
/yarn-debug.log*
|
||||
/yarn-error.log*
|
||||
/.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
||||
.env
|
||||
.env*.local
|
||||
/.env
|
||||
/.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
/.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
/*.tsbuildinfo
|
||||
2
app/.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
*.schema.json
|
||||
pnpm-lock.yaml
|
||||
@@ -11,7 +11,10 @@ declare module "nextjs-routes" {
|
||||
} from "next";
|
||||
|
||||
export type Route =
|
||||
| StaticRoute<"/account/signin">
|
||||
| DynamicRoute<"/api/[...trpc]", { "trpc": string[] }>
|
||||
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
|
||||
| StaticRoute<"/api/openapi">
|
||||
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
||||
| DynamicRoute<"/experiments/[id]", { "id": string }>
|
||||
| StaticRoute<"/experiments">
|
||||
@@ -19,7 +19,6 @@ FROM base as builder
|
||||
|
||||
# Include all NEXT_PUBLIC_* env vars here
|
||||
ARG NEXT_PUBLIC_POSTHOG_KEY
|
||||
ARG NEXT_PUBLIC_IS_PUBLIC_PLAYGROUND
|
||||
ARG NEXT_PUBLIC_SOCKET_URL
|
||||
|
||||
WORKDIR /app
|
||||
@@ -4,11 +4,18 @@
|
||||
|
||||
OpenPipe is a flexible playground for comparing and optimizing LLM prompts. It lets you quickly generate, test and compare candidate prompts with realistic sample data.
|
||||
|
||||
**Live Demo:** https://openpipe.ai
|
||||
## Sample Experiments
|
||||
|
||||
These are simple experiments users have created that show how OpenPipe works.
|
||||
|
||||
- [Country Capitals](https://app.openpipe.ai/experiments/11111111-1111-1111-1111-111111111111)
|
||||
- [Reddit User Needs](https://app.openpipe.ai/experiments/22222222-2222-2222-2222-222222222222)
|
||||
- [OpenAI Function Calls](https://app.openpipe.ai/experiments/2ebbdcb3-ed51-456e-87dc-91f72eaf3e2b)
|
||||
- [Activity Classification](https://app.openpipe.ai/experiments/3950940f-ab6b-4b74-841d-7e9dbc4e4ff8)
|
||||
|
||||
<img src="https://github.com/openpipe/openpipe/assets/176426/fc7624c6-5b65-4d4d-82b7-4a816f3e5678" alt="demo" height="400px">
|
||||
|
||||
Currently there's a public playground available at [https://openpipe.ai/](https://openpipe.ai/), but the recommended approach is to [run locally](#running-locally).
|
||||
You can use our hosted version of OpenPipe at [https://openpipe.ai]. You can also clone this repository and [run it locally](#running-locally).
|
||||
|
||||
## High-Level Features
|
||||
|
||||
@@ -36,7 +43,9 @@ Natively supports [OpenAI function calls](https://openai.com/blog/function-calli
|
||||
|
||||
## Supported Models
|
||||
|
||||
OpenPipe currently supports GPT-3.5 and GPT-4. Wider model support is planned.
|
||||
- All models available through the OpenAI [chat completion API](https://platform.openai.com/docs/guides/gpt/chat-completions-api)
|
||||
- Llama2 [7b chat](https://replicate.com/a16z-infra/llama7b-v2-chat), [13b chat](https://replicate.com/a16z-infra/llama13b-v2-chat), [70b chat](https://replicate.com/replicate/llama70b-v2-chat).
|
||||
- Anthropic's [Claude 1 Instant](https://www.anthropic.com/index/introducing-claude) and [Claude 2](https://www.anthropic.com/index/claude-2)
|
||||
|
||||
## Running Locally
|
||||
|
||||
@@ -47,5 +56,6 @@ OpenPipe currently supports GPT-3.5 and GPT-4. Wider model support is planned.
|
||||
5. Install the dependencies: `cd openpipe && pnpm install`
|
||||
6. Create a `.env` file (`cp .env.example .env`) and enter your `OPENAI_API_KEY`.
|
||||
7. Update `DATABASE_URL` if necessary to point to your Postgres instance and run `pnpm prisma db push` to create the database.
|
||||
8. Start the app: `pnpm dev`.
|
||||
9. Navigate to [http://localhost:3000](http://localhost:3000)
|
||||
8. Create a [GitHub OAuth App](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) and update the `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` values. (Note: a PR to make auth optional when running locally would be a great contribution!)
|
||||
9. Start the app: `pnpm dev`.
|
||||
10. Navigate to [http://localhost:3000](http://localhost:3000)
|
||||
1
app/dist/tsconfig.tsbuildinfo
vendored
Normal file
7
app/openapitools.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||
"spaces": 2,
|
||||
"generator-cli": {
|
||||
"version": "6.6.0"
|
||||
}
|
||||
}
|
||||
@@ -12,13 +12,17 @@
|
||||
"dev:next": "next dev",
|
||||
"dev:wss": "pnpm tsx --watch src/wss-server.ts",
|
||||
"dev:worker": "NODE_ENV='development' pnpm tsx --watch src/server/tasks/worker.ts",
|
||||
"dev": "concurrently --kill-others 'pnpm dev:next' 'pnpm dev:wss'",
|
||||
"dev": "concurrently --kill-others 'pnpm dev:next' 'pnpm dev:wss' 'pnpm dev:worker'",
|
||||
"postinstall": "prisma generate",
|
||||
"lint": "next lint",
|
||||
"start": "next start",
|
||||
"codegen": "tsx src/codegen/export-openai-types.ts"
|
||||
"codegen": "tsx src/codegen/export-client-types.ts",
|
||||
"seed": "tsx prisma/seed.ts",
|
||||
"check": "concurrently 'pnpm lint' 'pnpm tsc' 'pnpm prettier . --check'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.5.8",
|
||||
"@apidevtools/json-schema-ref-parser": "^10.1.0",
|
||||
"@babel/preset-typescript": "^7.22.5",
|
||||
"@babel/standalone": "^7.22.9",
|
||||
"@chakra-ui/next-js": "^2.1.4",
|
||||
@@ -26,6 +30,7 @@
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fontsource/inconsolata": "^5.0.5",
|
||||
"@monaco-editor/loader": "^1.3.3",
|
||||
"@next-auth/prisma-adapter": "^1.0.5",
|
||||
"@prisma/client": "^4.14.0",
|
||||
@@ -36,6 +41,7 @@
|
||||
"@trpc/next": "^10.26.0",
|
||||
"@trpc/react-query": "^10.26.0",
|
||||
"@trpc/server": "^10.26.0",
|
||||
"ast-types": "^0.14.2",
|
||||
"chroma-js": "^2.4.2",
|
||||
"concurrently": "^8.2.0",
|
||||
"cors": "^2.8.5",
|
||||
@@ -48,24 +54,38 @@
|
||||
"graphile-worker": "^0.13.0",
|
||||
"immer": "^10.0.2",
|
||||
"isolated-vm": "^4.5.0",
|
||||
"json-schema-to-typescript": "^13.0.2",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"jsonschema": "^1.4.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"next": "^13.4.2",
|
||||
"next-auth": "^4.22.1",
|
||||
"next-query-params": "^4.2.3",
|
||||
"nextjs-cors": "^2.1.2",
|
||||
"nextjs-routes": "^2.0.1",
|
||||
"openai": "4.0.0-beta.2",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "^1.68.4",
|
||||
"prettier": "^3.0.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "18.2.0",
|
||||
"react-diff-viewer": "^3.1.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-icons": "^4.10.1",
|
||||
"react-select": "^5.7.4",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-textarea-autosize": "^8.5.0",
|
||||
"recast": "^0.23.3",
|
||||
"replicate": "^0.12.3",
|
||||
"socket.io": "^4.7.1",
|
||||
"socket.io-client": "^4.7.1",
|
||||
"superjson": "1.12.2",
|
||||
"trpc-openapi": "^1.2.0",
|
||||
"tsx": "^3.12.7",
|
||||
"type-fest": "^4.0.0",
|
||||
"use-query-params": "^2.2.1",
|
||||
"uuid": "^9.0.0",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"zod": "^3.21.4",
|
||||
"zustand": "^4.3.9"
|
||||
},
|
||||
@@ -77,12 +97,15 @@
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/eslint": "^8.37.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/lodash-es": "^4.17.8",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/pluralize": "^0.0.30",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@types/react-syntax-highlighter": "^15.5.7",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
||||
"@typescript-eslint/parser": "^5.59.6",
|
||||
"eslint": "^8.40.0",
|
||||
@@ -100,6 +123,6 @@
|
||||
"initVersion": "7.14.0"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "tsx prisma/seed.ts"
|
||||
"seed": "pnpm seed"
|
||||
}
|
||||
}
|
||||
1119
pnpm-lock.yaml → app/pnpm-lock.yaml
generated
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "PromptVariant" ALTER COLUMN "model" DROP DEFAULT;
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to rename the column `matchString` on the `Evaluation` table. If there is any code or views referring to the old name, they will break.
|
||||
- You are about to rename the column `matchType` on the `Evaluation` table. If there is any code or views referring to the old name, they will break.
|
||||
- You are about to rename the column `name` on the `Evaluation` table. If there is any code or views referring to the old name, they will break.
|
||||
- You are about to rename the enum `EvaluationMatchType` to `EvalType`. If there is any code or views referring to the old name, they will break.
|
||||
*/
|
||||
|
||||
-- RenameEnum
|
||||
ALTER TYPE "EvaluationMatchType" RENAME TO "EvalType";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Evaluation" RENAME COLUMN "matchString" TO "value";
|
||||
ALTER TABLE "Evaluation" RENAME COLUMN "matchType" TO "evalType";
|
||||
ALTER TABLE "Evaluation" RENAME COLUMN "name" TO "label";
|
||||
|
||||
-- AlterColumnType
|
||||
ALTER TABLE "Evaluation" ALTER COLUMN "evalType" TYPE "EvalType" USING "evalType"::text::"EvalType";
|
||||
|
||||
-- SetNotNullConstraint
|
||||
ALTER TABLE "Evaluation" ALTER COLUMN "evalType" SET NOT NULL;
|
||||
ALTER TABLE "Evaluation" ALTER COLUMN "label" SET NOT NULL;
|
||||
ALTER TABLE "Evaluation" ALTER COLUMN "value" SET NOT NULL;
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `EvaluationResult` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- AlterEnum
|
||||
ALTER TYPE "EvalType" ADD VALUE 'GPT4_EVAL';
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "EvaluationResult" DROP CONSTRAINT "EvaluationResult_evaluationId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "EvaluationResult" DROP CONSTRAINT "EvaluationResult_promptVariantId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "EvaluationResult";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OutputEvaluation" (
|
||||
"id" UUID NOT NULL,
|
||||
"result" DOUBLE PRECISION NOT NULL,
|
||||
"details" TEXT,
|
||||
"modelOutputId" UUID NOT NULL,
|
||||
"evaluationId" UUID NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "OutputEvaluation_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OutputEvaluation_modelOutputId_evaluationId_key" ON "OutputEvaluation"("modelOutputId", "evaluationId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OutputEvaluation" ADD CONSTRAINT "OutputEvaluation_modelOutputId_fkey" FOREIGN KEY ("modelOutputId") REFERENCES "ModelOutput"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OutputEvaluation" ADD CONSTRAINT "OutputEvaluation_evaluationId_fkey" FOREIGN KEY ("evaluationId") REFERENCES "Evaluation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,124 @@
|
||||
DROP TABLE "Account";
|
||||
DROP TABLE "Session";
|
||||
DROP TABLE "User";
|
||||
DROP TABLE "VerificationToken";
|
||||
|
||||
CREATE TYPE "OrganizationUserRole" AS ENUM ('ADMIN', 'MEMBER', 'VIEWER');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Organization" (
|
||||
"id" UUID NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"personalOrgUserId" UUID,
|
||||
|
||||
CONSTRAINT "Organization_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OrganizationUser" (
|
||||
"id" UUID NOT NULL,
|
||||
"role" "OrganizationUserRole" NOT NULL,
|
||||
"organizationId" UUID NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "OrganizationUser_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Account" (
|
||||
"id" UUID NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"providerAccountId" TEXT NOT NULL,
|
||||
"refresh_token" TEXT,
|
||||
"refresh_token_expires_in" INTEGER,
|
||||
"access_token" TEXT,
|
||||
"expires_at" INTEGER,
|
||||
"token_type" TEXT,
|
||||
"scope" TEXT,
|
||||
"id_token" TEXT,
|
||||
"session_state" TEXT,
|
||||
|
||||
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" UUID NOT NULL,
|
||||
"sessionToken" TEXT NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
"expires" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT,
|
||||
"email" TEXT,
|
||||
"emailVerified" TIMESTAMP(3),
|
||||
"image" TEXT,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "VerificationToken" (
|
||||
"identifier" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"expires" TIMESTAMP(3) NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "Organization" ("id", "updatedAt") VALUES ('11111111-1111-1111-1111-111111111111', CURRENT_TIMESTAMP);
|
||||
|
||||
-- AlterTable add organizationId as NULLABLE
|
||||
ALTER TABLE "Experiment" ADD COLUMN "organizationId" UUID;
|
||||
|
||||
-- Set default organization for existing experiments
|
||||
UPDATE "Experiment" SET "organizationId" = '11111111-1111-1111-1111-111111111111';
|
||||
|
||||
-- AlterTable set organizationId as NOT NULL
|
||||
ALTER TABLE "Experiment" ALTER COLUMN "organizationId" SET NOT NULL;
|
||||
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OrganizationUser_organizationId_userId_key" ON "OrganizationUser"("organizationId", "userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Experiment" ADD CONSTRAINT "Experiment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrganizationUser" ADD CONSTRAINT "OrganizationUser_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrganizationUser" ADD CONSTRAINT "OrganizationUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
CREATE UNIQUE INDEX "Organization_personalOrgUserId_key" ON "Organization"("personalOrgUserId");
|
||||
|
||||
ALTER TABLE "Organization" ADD CONSTRAINT "Organization_personalOrgUserId_fkey" FOREIGN KEY ("personalOrgUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `completionTokens` on the `ScenarioVariantCell` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `inputHash` on the `ScenarioVariantCell` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `output` on the `ScenarioVariantCell` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `promptTokens` on the `ScenarioVariantCell` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `timeToComplete` on the `ScenarioVariantCell` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "ScenarioVariantCell" DROP COLUMN "completionTokens",
|
||||
DROP COLUMN "inputHash",
|
||||
DROP COLUMN "output",
|
||||
DROP COLUMN "promptTokens",
|
||||
DROP COLUMN "timeToComplete";
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `model` on the `PromptVariant` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "ModelOutput" ADD COLUMN "cost" DOUBLE PRECISION;
|
||||
@@ -0,0 +1,17 @@
|
||||
-- Add new columns allowing NULL values
|
||||
ALTER TABLE "PromptVariant"
|
||||
ADD COLUMN "constructFnVersion" INTEGER,
|
||||
ADD COLUMN "modelProvider" TEXT;
|
||||
|
||||
-- Update existing records to have the default values
|
||||
UPDATE "PromptVariant"
|
||||
SET "constructFnVersion" = 1,
|
||||
"modelProvider" = 'openai/ChatCompletion'
|
||||
WHERE "constructFnVersion" IS NULL OR "modelProvider" IS NULL;
|
||||
|
||||
-- Alter table to set NOT NULL constraint
|
||||
ALTER TABLE "PromptVariant"
|
||||
ALTER COLUMN "constructFnVersion" SET NOT NULL,
|
||||
ALTER COLUMN "modelProvider" SET NOT NULL;
|
||||
|
||||
ALTER TABLE "ScenarioVariantCell" ADD COLUMN "prompt" JSONB;
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `streamingChannel` on the `ScenarioVariantCell` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "ScenarioVariantCell" DROP COLUMN "streamingChannel";
|
||||
@@ -0,0 +1,52 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "ModelOutput" DROP CONSTRAINT "ModelOutput_scenarioVariantCellId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "OutputEvaluation" DROP CONSTRAINT "OutputEvaluation_modelOutputId_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "OutputEvaluation_modelOutputId_evaluationId_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "OutputEvaluation" RENAME COLUMN "modelOutputId" TO "modelResponseId";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ScenarioVariantCell" DROP COLUMN "retryTime",
|
||||
DROP COLUMN "statusCode",
|
||||
ADD COLUMN "jobQueuedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "jobStartedAt" TIMESTAMP(3);
|
||||
|
||||
ALTER TABLE "ModelOutput" RENAME TO "ModelResponse";
|
||||
|
||||
ALTER TABLE "ModelResponse"
|
||||
ADD COLUMN "requestedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "receivedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "statusCode" INTEGER,
|
||||
ADD COLUMN "errorMessage" TEXT,
|
||||
ADD COLUMN "retryTime" TIMESTAMP(3),
|
||||
ADD COLUMN "outdated" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- 3. Remove the unnecessary column
|
||||
ALTER TABLE "ModelResponse"
|
||||
DROP COLUMN "timeToComplete";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ModelResponse" RENAME CONSTRAINT "ModelOutput_pkey" TO "ModelResponse_pkey";
|
||||
ALTER TABLE "ModelResponse" ALTER COLUMN "output" DROP NOT NULL;
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "ModelOutput_scenarioVariantCellId_key";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ModelResponse" ADD CONSTRAINT "ModelResponse_scenarioVariantCellId_fkey" FOREIGN KEY ("scenarioVariantCellId") REFERENCES "ScenarioVariantCell"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "ModelOutput_inputHash_idx" RENAME TO "ModelResponse_inputHash_idx";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OutputEvaluation_modelResponseId_evaluationId_key" ON "OutputEvaluation"("modelResponseId", "evaluationId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OutputEvaluation" ADD CONSTRAINT "OutputEvaluation_modelResponseId_fkey" FOREIGN KEY ("modelResponseId") REFERENCES "ModelResponse"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Experiment" ADD COLUMN "dataFlowId" UUID;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ModelResponse" ADD COLUMN "loggedCallId" UUID,
|
||||
ALTER COLUMN "scenarioVariantCellId" DROP NOT NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LoggedCall" (
|
||||
"id" UUID NOT NULL,
|
||||
"promptFunctionHash" TEXT NOT NULL,
|
||||
"promptFunction" TEXT NOT NULL,
|
||||
"prompt" JSONB NOT NULL,
|
||||
"responsePayload" JSONB,
|
||||
"scenarioVariables" JSONB NOT NULL,
|
||||
"model" TEXT NOT NULL,
|
||||
"modelProvider" TEXT NOT NULL,
|
||||
"dataFlowId" UUID NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "LoggedCall_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "DataFlow" (
|
||||
"id" UUID NOT NULL,
|
||||
"label" TEXT NOT NULL,
|
||||
"organizationId" UUID NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "DataFlow_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ApiKey" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"organizationId" UUID NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ApiKey_key_key" ON "ApiKey"("key");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Experiment" ADD CONSTRAINT "Experiment_dataFlowId_fkey" FOREIGN KEY ("dataFlowId") REFERENCES "DataFlow"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ModelResponse" ADD CONSTRAINT "ModelResponse_loggedCallId_fkey" FOREIGN KEY ("loggedCallId") REFERENCES "LoggedCall"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LoggedCall" ADD CONSTRAINT "LoggedCall_dataFlowId_fkey" FOREIGN KEY ("dataFlowId") REFERENCES "DataFlow"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "DataFlow" ADD CONSTRAINT "DataFlow_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
309
app/prisma/schema.prisma
Normal file
@@ -0,0 +1,309 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Experiment {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
label String
|
||||
|
||||
sortIndex Int @default(0)
|
||||
|
||||
organizationId String @db.Uuid
|
||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
|
||||
dataFlowId String? @db.Uuid
|
||||
dataFlow DataFlow? @relation(fields: [dataFlowId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
templateVariables TemplateVariable[]
|
||||
promptVariants PromptVariant[]
|
||||
testScenarios TestScenario[]
|
||||
evaluations Evaluation[]
|
||||
}
|
||||
|
||||
model PromptVariant {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
|
||||
label String
|
||||
constructFn String
|
||||
constructFnVersion Int
|
||||
model String
|
||||
modelProvider String
|
||||
|
||||
uiId String @default(uuid()) @db.Uuid
|
||||
visible Boolean @default(true)
|
||||
sortIndex Int @default(0)
|
||||
|
||||
experimentId String @db.Uuid
|
||||
experiment Experiment @relation(fields: [experimentId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
scenarioVariantCells ScenarioVariantCell[]
|
||||
|
||||
@@index([uiId])
|
||||
}
|
||||
|
||||
model TestScenario {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
|
||||
variableValues Json
|
||||
|
||||
uiId String @default(uuid()) @db.Uuid
|
||||
visible Boolean @default(true)
|
||||
sortIndex Int @default(0)
|
||||
|
||||
experimentId String @db.Uuid
|
||||
experiment Experiment @relation(fields: [experimentId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
scenarioVariantCells ScenarioVariantCell[]
|
||||
}
|
||||
|
||||
model TemplateVariable {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
|
||||
label String
|
||||
|
||||
experimentId String @db.Uuid
|
||||
experiment Experiment @relation(fields: [experimentId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
enum CellRetrievalStatus {
|
||||
PENDING
|
||||
IN_PROGRESS
|
||||
COMPLETE
|
||||
ERROR
|
||||
}
|
||||
|
||||
model ScenarioVariantCell {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
|
||||
retrievalStatus CellRetrievalStatus @default(COMPLETE)
|
||||
jobQueuedAt DateTime?
|
||||
jobStartedAt DateTime?
|
||||
modelResponses ModelResponse[]
|
||||
errorMessage String? // Contains errors that occurred independently of model responses
|
||||
|
||||
promptVariantId String @db.Uuid
|
||||
promptVariant PromptVariant @relation(fields: [promptVariantId], references: [id], onDelete: Cascade)
|
||||
prompt Json?
|
||||
|
||||
testScenarioId String @db.Uuid
|
||||
testScenario TestScenario @relation(fields: [testScenarioId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([promptVariantId, testScenarioId])
|
||||
}
|
||||
|
||||
model ModelResponse {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
|
||||
inputHash String
|
||||
requestedAt DateTime?
|
||||
receivedAt DateTime?
|
||||
output Json?
|
||||
cost Float?
|
||||
promptTokens Int?
|
||||
completionTokens Int?
|
||||
statusCode Int?
|
||||
errorMessage String?
|
||||
retryTime DateTime?
|
||||
outdated Boolean @default(false)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
scenarioVariantCellId String? @db.Uuid
|
||||
scenarioVariantCell ScenarioVariantCell? @relation(fields: [scenarioVariantCellId], references: [id], onDelete: Cascade)
|
||||
outputEvaluations OutputEvaluation[]
|
||||
|
||||
loggedCallId String? @db.Uuid
|
||||
loggedCall LoggedCall? @relation(fields: [loggedCallId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([inputHash])
|
||||
}
|
||||
|
||||
enum EvalType {
|
||||
CONTAINS
|
||||
DOES_NOT_CONTAIN
|
||||
GPT4_EVAL
|
||||
}
|
||||
|
||||
model Evaluation {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
|
||||
label String
|
||||
evalType EvalType
|
||||
value String
|
||||
|
||||
experimentId String @db.Uuid
|
||||
experiment Experiment @relation(fields: [experimentId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
outputEvaluations OutputEvaluation[]
|
||||
}
|
||||
|
||||
model OutputEvaluation {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
|
||||
// Number between 0 (fail) and 1 (pass)
|
||||
result Float
|
||||
details String?
|
||||
|
||||
modelResponseId String @db.Uuid
|
||||
modelResponse ModelResponse @relation(fields: [modelResponseId], references: [id], onDelete: Cascade)
|
||||
|
||||
evaluationId String @db.Uuid
|
||||
evaluation Evaluation @relation(fields: [evaluationId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([modelResponseId, evaluationId])
|
||||
}
|
||||
|
||||
model LoggedCall {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
|
||||
promptFunctionHash String
|
||||
promptFunction String
|
||||
prompt Json
|
||||
responsePayload Json?
|
||||
scenarioVariables Json
|
||||
model String
|
||||
modelProvider String
|
||||
|
||||
dataFlowId String @db.Uuid
|
||||
dataFlow DataFlow @relation(fields: [dataFlowId], references: [id], onDelete: Cascade)
|
||||
|
||||
modelResponse ModelResponse[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model DataFlow {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
|
||||
label String
|
||||
experiments Experiment[]
|
||||
loggedCalls LoggedCall[]
|
||||
organizationId String @db.Uuid
|
||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ApiKey {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
|
||||
name String
|
||||
key String @unique
|
||||
organizationId String @db.Uuid
|
||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Organization {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
personalOrgUserId String? @unique @db.Uuid
|
||||
personalOrgUser User? @relation(fields: [personalOrgUserId], references: [id], onDelete: Cascade)
|
||||
apiKeys ApiKey[]
|
||||
dataFlows DataFlow[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
organizationUsers OrganizationUser[]
|
||||
experiments Experiment[]
|
||||
}
|
||||
|
||||
enum OrganizationUserRole {
|
||||
ADMIN
|
||||
MEMBER
|
||||
VIEWER
|
||||
}
|
||||
|
||||
model OrganizationUser {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
|
||||
role OrganizationUserRole
|
||||
|
||||
organizationId String @db.Uuid
|
||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
|
||||
userId String @db.Uuid
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([organizationId, userId])
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
userId String @db.Uuid
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? @db.Text
|
||||
refresh_token_expires_in Int?
|
||||
access_token String? @db.Text
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? @db.Text
|
||||
session_state String?
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
sessionToken String @unique
|
||||
userId String @db.Uuid
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
organizationUsers OrganizationUser[]
|
||||
organizations Organization[]
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
168
app/prisma/seed.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { prisma } from "~/server/db";
|
||||
import dedent from "dedent";
|
||||
import { generateNewCell } from "~/server/utils/generateNewCell";
|
||||
|
||||
const defaultId = "11111111-1111-1111-1111-111111111111";
|
||||
|
||||
await prisma.organization.deleteMany({
|
||||
where: { id: defaultId },
|
||||
});
|
||||
|
||||
// If there's an existing org, just seed into it
|
||||
const org =
|
||||
(await prisma.organization.findFirst({})) ??
|
||||
(await prisma.organization.create({
|
||||
data: { id: defaultId },
|
||||
}));
|
||||
|
||||
await prisma.experiment.deleteMany({
|
||||
where: {
|
||||
id: defaultId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.experiment.create({
|
||||
data: {
|
||||
id: defaultId,
|
||||
label: "Country Capitals Example",
|
||||
organizationId: org.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.scenarioVariantCell.deleteMany({
|
||||
where: {
|
||||
promptVariant: {
|
||||
experimentId: defaultId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.promptVariant.deleteMany({
|
||||
where: {
|
||||
experimentId: defaultId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.promptVariant.createMany({
|
||||
data: [
|
||||
{
|
||||
experimentId: defaultId,
|
||||
label: "Prompt Variant 1",
|
||||
sortIndex: 0,
|
||||
model: "gpt-3.5-turbo-0613",
|
||||
modelProvider: "openai/ChatCompletion",
|
||||
constructFnVersion: 1,
|
||||
constructFn: dedent`
|
||||
definePrompt("openai/ChatCompletion", {
|
||||
model: "gpt-3.5-turbo-0613",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: \`What is the capital of ${"$"}{scenario.country}?\`
|
||||
}
|
||||
],
|
||||
temperature: 0,
|
||||
})`,
|
||||
},
|
||||
{
|
||||
experimentId: defaultId,
|
||||
label: "Prompt Variant 2",
|
||||
sortIndex: 1,
|
||||
model: "gpt-3.5-turbo-0613",
|
||||
modelProvider: "openai/ChatCompletion",
|
||||
constructFnVersion: 1,
|
||||
constructFn: dedent`
|
||||
definePrompt("openai/ChatCompletion", {
|
||||
model: "gpt-3.5-turbo-0613",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: \`What is the capital of ${"$"}{scenario.country}? Return just the city name and nothing else.\`
|
||||
}
|
||||
],
|
||||
temperature: 0,
|
||||
})`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.templateVariable.deleteMany({
|
||||
where: {
|
||||
experimentId: defaultId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.templateVariable.createMany({
|
||||
data: [
|
||||
{
|
||||
experimentId: defaultId,
|
||||
label: "country",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.testScenario.deleteMany({
|
||||
where: {
|
||||
experimentId: defaultId,
|
||||
},
|
||||
});
|
||||
|
||||
const countries = [
|
||||
"Afghanistan",
|
||||
"Albania",
|
||||
"Algeria",
|
||||
"Andorra",
|
||||
"Angola",
|
||||
"Antigua and Barbuda",
|
||||
"Argentina",
|
||||
"Armenia",
|
||||
"Australia",
|
||||
"Austria",
|
||||
"Austrian Empire",
|
||||
"Azerbaijan",
|
||||
"Baden",
|
||||
"Bahamas, The",
|
||||
"Bahrain",
|
||||
"Bangladesh",
|
||||
"Barbados",
|
||||
"Bavaria",
|
||||
"Belarus",
|
||||
"Belgium",
|
||||
"Belize",
|
||||
"Benin (Dahomey)",
|
||||
"Bolivia",
|
||||
"Bosnia and Herzegovina",
|
||||
"Botswana",
|
||||
];
|
||||
await prisma.testScenario.createMany({
|
||||
data: countries.map((country, i) => ({
|
||||
experimentId: defaultId,
|
||||
sortIndex: i,
|
||||
variableValues: {
|
||||
country: country,
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
const variants = await prisma.promptVariant.findMany({
|
||||
where: {
|
||||
experimentId: defaultId,
|
||||
},
|
||||
});
|
||||
|
||||
const scenarios = await prisma.testScenario.findMany({
|
||||
where: {
|
||||
experimentId: defaultId,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
variants
|
||||
.flatMap((variant) =>
|
||||
scenarios.map((scenario) => ({
|
||||
promptVariantId: variant.id,
|
||||
testScenarioId: scenario.id,
|
||||
})),
|
||||
)
|
||||
.map((cell) => generateNewCell(cell.promptVariantId, cell.testScenarioId, { stream: false })),
|
||||
);
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 704 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 858 B After Width: | Height: | Size: 858 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -12,7 +12,7 @@ services:
|
||||
dockerContext: .
|
||||
plan: standard
|
||||
domains:
|
||||
- openpipe.ai
|
||||
- app.openpipe.ai
|
||||
envVars:
|
||||
- key: NODE_ENV
|
||||
value: production
|
||||
@@ -6,4 +6,7 @@ echo "Migrating the database"
|
||||
pnpm prisma migrate deploy
|
||||
|
||||
echo "Starting the server"
|
||||
pnpm start
|
||||
|
||||
pnpm concurrently --kill-others \
|
||||
"pnpm start" \
|
||||
"pnpm tsx src/server/tasks/worker.ts"
|
||||
42
app/src/codegen/export-client-types.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import "dotenv/config";
|
||||
import { openApiDocument } from "~/pages/api/openapi.json";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { execSync } from "child_process";
|
||||
import { generatePromptTypes } from "~/modelProviders/generatePromptTypes";
|
||||
|
||||
console.log("Exporting public OpenAPI schema to client-libs/schema.json");
|
||||
|
||||
const executionDir = import.meta.url.replace("file://", "");
|
||||
const schemaPath = path.join(
|
||||
path.dirname(executionDir),
|
||||
"../../../client-libs/schema.json"
|
||||
);
|
||||
|
||||
console.log("Exporting schema");
|
||||
fs.writeFileSync(schemaPath, JSON.stringify(openApiDocument, null, 2), "utf-8");
|
||||
|
||||
console.log("Generating Typescript client");
|
||||
|
||||
const tsClientPath = path.join(
|
||||
path.dirname(executionDir),
|
||||
"../../../client-libs/js/codegen"
|
||||
);
|
||||
|
||||
fs.rmSync(tsClientPath, { recursive: true, force: true });
|
||||
|
||||
execSync(
|
||||
`pnpm dlx @openapitools/openapi-generator-cli generate -i "${schemaPath}" -g typescript-axios -o "${tsClientPath}"`,
|
||||
{
|
||||
stdio: "inherit",
|
||||
}
|
||||
);
|
||||
|
||||
const promptTypes = await generatePromptTypes();
|
||||
|
||||
const promptTypesPath = path.join(tsClientPath, "promptTypes.ts");
|
||||
|
||||
console.log(`Writing promptTypes to ${promptTypesPath}`);
|
||||
fs.writeFileSync(promptTypesPath, promptTypes, "utf-8");
|
||||
|
||||
console.log("Done!");
|
||||
@@ -1,19 +1,22 @@
|
||||
import { Textarea, type TextareaProps } from "@chakra-ui/react";
|
||||
import ResizeTextarea from "react-textarea-autosize";
|
||||
import React from "react";
|
||||
import React, { useLayoutEffect, useState } from "react";
|
||||
|
||||
export const AutoResizeTextarea: React.ForwardRefRenderFunction<
|
||||
HTMLTextAreaElement,
|
||||
TextareaProps
|
||||
> = (props, ref) => {
|
||||
TextareaProps & { minRows?: number }
|
||||
> = ({ minRows = 1, overflowY = "hidden", ...props }, ref) => {
|
||||
const [isRerendered, setIsRerendered] = useState(false);
|
||||
useLayoutEffect(() => setIsRerendered(true), []);
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
minH="unset"
|
||||
overflow="hidden"
|
||||
minRows={minRows}
|
||||
overflowY={isRerendered ? overflowY : "hidden"}
|
||||
w="100%"
|
||||
resize="none"
|
||||
ref={ref}
|
||||
minRows={1}
|
||||
transition="height none"
|
||||
as={ResizeTextarea}
|
||||
{...props}
|
||||
142
app/src/components/ChangeModelModal/ChangeModelModal.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
Button,
|
||||
HStack,
|
||||
Icon,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Spinner,
|
||||
Text,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { type PromptVariant } from "@prisma/client";
|
||||
import { isObject, isString } from "lodash-es";
|
||||
import { useState } from "react";
|
||||
import { RiExchangeFundsFill } from "react-icons/ri";
|
||||
import { type ProviderModel } from "~/modelProviders/types";
|
||||
import { api } from "~/utils/api";
|
||||
import { useExperiment, useHandledAsyncCallback, useVisibleScenarioIds } from "~/utils/hooks";
|
||||
import { lookupModel, modelLabel } from "~/utils/utils";
|
||||
import CompareFunctions from "../RefinePromptModal/CompareFunctions";
|
||||
import { ModelSearch } from "./ModelSearch";
|
||||
import { ModelStatsCard } from "./ModelStatsCard";
|
||||
|
||||
export const ChangeModelModal = ({
|
||||
variant,
|
||||
onClose,
|
||||
}: {
|
||||
variant: PromptVariant;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const originalModel = lookupModel(variant.modelProvider, variant.model);
|
||||
const [selectedModel, setSelectedModel] = useState({
|
||||
provider: variant.modelProvider,
|
||||
model: variant.model,
|
||||
} as ProviderModel);
|
||||
const [convertedModel, setConvertedModel] = useState<ProviderModel | undefined>();
|
||||
const visibleScenarios = useVisibleScenarioIds();
|
||||
|
||||
const utils = api.useContext();
|
||||
|
||||
const experiment = useExperiment();
|
||||
|
||||
const { mutateAsync: getModifiedPromptMutateAsync, data: modifiedPromptFn } =
|
||||
api.promptVariants.getModifiedPromptFn.useMutation();
|
||||
|
||||
const [getModifiedPromptFn, modificationInProgress] = useHandledAsyncCallback(async () => {
|
||||
if (!experiment) return;
|
||||
|
||||
await getModifiedPromptMutateAsync({
|
||||
id: variant.id,
|
||||
newModel: selectedModel,
|
||||
});
|
||||
setConvertedModel(selectedModel);
|
||||
}, [getModifiedPromptMutateAsync, onClose, experiment, variant, selectedModel]);
|
||||
|
||||
const replaceVariantMutation = api.promptVariants.replaceVariant.useMutation();
|
||||
|
||||
const [replaceVariant, replacementInProgress] = useHandledAsyncCallback(async () => {
|
||||
if (
|
||||
!variant.experimentId ||
|
||||
!modifiedPromptFn ||
|
||||
(isObject(modifiedPromptFn) && "status" in modifiedPromptFn)
|
||||
)
|
||||
return;
|
||||
await replaceVariantMutation.mutateAsync({
|
||||
id: variant.id,
|
||||
constructFn: modifiedPromptFn,
|
||||
streamScenarios: visibleScenarios,
|
||||
});
|
||||
await utils.promptVariants.list.invalidate();
|
||||
onClose();
|
||||
}, [replaceVariantMutation, variant, onClose, modifiedPromptFn]);
|
||||
|
||||
const originalLabel = modelLabel(variant.modelProvider, variant.model);
|
||||
const selectedLabel = modelLabel(selectedModel.provider, selectedModel.model);
|
||||
const convertedLabel =
|
||||
convertedModel && modelLabel(convertedModel.provider, convertedModel.model);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
size={{ base: "xl", sm: "2xl", md: "3xl", lg: "5xl", xl: "7xl" }}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent w={1200}>
|
||||
<ModalHeader>
|
||||
<HStack>
|
||||
<Icon as={RiExchangeFundsFill} />
|
||||
<Text>Change Model</Text>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody maxW="unset">
|
||||
<VStack spacing={8}>
|
||||
<ModelStatsCard label="Original Model" model={originalModel} />
|
||||
{originalLabel !== selectedLabel && (
|
||||
<ModelStatsCard
|
||||
label="New Model"
|
||||
model={lookupModel(selectedModel.provider, selectedModel.model)}
|
||||
/>
|
||||
)}
|
||||
<ModelSearch selectedModel={selectedModel} setSelectedModel={setSelectedModel} />
|
||||
{isString(modifiedPromptFn) && (
|
||||
<CompareFunctions
|
||||
originalFunction={variant.constructFn}
|
||||
newFunction={modifiedPromptFn}
|
||||
leftTitle={originalLabel}
|
||||
rightTitle={convertedLabel}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<HStack>
|
||||
<Button
|
||||
colorScheme="gray"
|
||||
onClick={getModifiedPromptFn}
|
||||
minW={24}
|
||||
isDisabled={originalLabel === selectedLabel || modificationInProgress}
|
||||
>
|
||||
{modificationInProgress ? <Spinner boxSize={4} /> : <Text>Convert</Text>}
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
onClick={replaceVariant}
|
||||
minW={24}
|
||||
isDisabled={!convertedModel || modificationInProgress || replacementInProgress}
|
||||
>
|
||||
{replacementInProgress ? <Spinner boxSize={4} /> : <Text>Accept</Text>}
|
||||
</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
36
app/src/components/ChangeModelModal/ModelSearch.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Text, VStack } from "@chakra-ui/react";
|
||||
import { type LegacyRef } from "react";
|
||||
import Select from "react-select";
|
||||
import { useElementDimensions } from "~/utils/hooks";
|
||||
|
||||
import { flatMap } from "lodash-es";
|
||||
import frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
||||
import { type ProviderModel } from "~/modelProviders/types";
|
||||
import { modelLabel } from "~/utils/utils";
|
||||
|
||||
const modelOptions = flatMap(Object.entries(frontendModelProviders), ([providerId, provider]) =>
|
||||
Object.entries(provider.models).map(([modelId]) => ({
|
||||
provider: providerId,
|
||||
model: modelId,
|
||||
})),
|
||||
) as ProviderModel[];
|
||||
|
||||
export const ModelSearch = (props: {
|
||||
selectedModel: ProviderModel;
|
||||
setSelectedModel: (model: ProviderModel) => void;
|
||||
}) => {
|
||||
const [containerRef, containerDimensions] = useElementDimensions();
|
||||
|
||||
return (
|
||||
<VStack ref={containerRef as LegacyRef<HTMLDivElement>} w="full">
|
||||
<Text>Browse Models</Text>
|
||||
<Select<ProviderModel>
|
||||
styles={{ control: (provided) => ({ ...provided, width: containerDimensions?.width }) }}
|
||||
getOptionLabel={(data) => modelLabel(data.provider, data.model)}
|
||||
getOptionValue={(data) => modelLabel(data.provider, data.model)}
|
||||
options={modelOptions}
|
||||
onChange={(option) => option && props.setSelectedModel(option)}
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
109
app/src/components/ChangeModelModal/ModelStatsCard.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
GridItem,
|
||||
HStack,
|
||||
Link,
|
||||
SimpleGrid,
|
||||
Text,
|
||||
VStack,
|
||||
type StackProps,
|
||||
} from "@chakra-ui/react";
|
||||
import { type lookupModel } from "~/utils/utils";
|
||||
|
||||
export const ModelStatsCard = ({
|
||||
label,
|
||||
model,
|
||||
}: {
|
||||
label: string;
|
||||
model: ReturnType<typeof lookupModel>;
|
||||
}) => {
|
||||
if (!model) return null;
|
||||
return (
|
||||
<VStack w="full" align="start">
|
||||
<Text fontWeight="bold" fontSize="sm" textTransform="uppercase">
|
||||
{label}
|
||||
</Text>
|
||||
|
||||
<VStack w="full" spacing={6} bgColor="gray.100" p={4} borderRadius={4}>
|
||||
<HStack w="full" align="flex-start">
|
||||
<Text flex={1} fontSize="lg">
|
||||
<Text as="span" color="gray.600">
|
||||
{model.provider} /{" "}
|
||||
</Text>
|
||||
<Text as="span" fontWeight="bold" color="gray.900">
|
||||
{model.name}
|
||||
</Text>
|
||||
</Text>
|
||||
<Link
|
||||
href={model.learnMoreUrl}
|
||||
isExternal
|
||||
color="blue.500"
|
||||
fontWeight="bold"
|
||||
fontSize="sm"
|
||||
ml={2}
|
||||
>
|
||||
Learn More
|
||||
</Link>
|
||||
</HStack>
|
||||
<SimpleGrid
|
||||
w="full"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-start"
|
||||
fontSize="sm"
|
||||
columns={{ base: 2, md: 4 }}
|
||||
>
|
||||
<SelectedModelLabeledInfo label="Context Window" info={model.contextWindow} />
|
||||
{model.promptTokenPrice && (
|
||||
<SelectedModelLabeledInfo
|
||||
label="Input"
|
||||
info={
|
||||
<Text>
|
||||
${(model.promptTokenPrice * 1000).toFixed(3)}
|
||||
<Text color="gray.500"> / 1K tokens</Text>
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{model.completionTokenPrice && (
|
||||
<SelectedModelLabeledInfo
|
||||
label="Output"
|
||||
info={
|
||||
<Text>
|
||||
${(model.completionTokenPrice * 1000).toFixed(3)}
|
||||
<Text color="gray.500"> / 1K tokens</Text>
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{model.pricePerSecond && (
|
||||
<SelectedModelLabeledInfo
|
||||
label="Price"
|
||||
info={
|
||||
<Text>
|
||||
${model.pricePerSecond.toFixed(3)}
|
||||
<Text color="gray.500"> / second</Text>
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<SelectedModelLabeledInfo label="Speed" info={<Text>{model.speed}</Text>} />
|
||||
</SimpleGrid>
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectedModelLabeledInfo = ({
|
||||
label,
|
||||
info,
|
||||
...props
|
||||
}: {
|
||||
label: string;
|
||||
info: string | number | React.ReactElement;
|
||||
} & StackProps) => (
|
||||
<GridItem>
|
||||
<VStack alignItems="flex-start" {...props}>
|
||||
<Text fontWeight="bold">{label}</Text>
|
||||
<Text>{info}</Text>
|
||||
</VStack>
|
||||
</GridItem>
|
||||
);
|
||||
69
app/src/components/ExperimentSettingsDrawer/DeleteButton.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogContent,
|
||||
AlertDialogOverlay,
|
||||
useDisclosure,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import { useRef } from "react";
|
||||
import { BsTrash } from "react-icons/bs";
|
||||
import { api } from "~/utils/api";
|
||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||
|
||||
export const DeleteButton = () => {
|
||||
const experiment = useExperiment();
|
||||
const mutation = api.experiments.delete.useMutation();
|
||||
const utils = api.useContext();
|
||||
const router = useRouter();
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const [onDeleteConfirm] = useHandledAsyncCallback(async () => {
|
||||
if (!experiment.data?.id) return;
|
||||
await mutation.mutateAsync({ id: experiment.data.id });
|
||||
await utils.experiments.list.invalidate();
|
||||
await router.push({ pathname: "/experiments" });
|
||||
onClose();
|
||||
}, [mutation, experiment.data?.id, router]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" variant="ghost" colorScheme="red" fontWeight="normal" onClick={onOpen}>
|
||||
<Icon as={BsTrash} boxSize={4} />
|
||||
<Text ml={2}>Delete Experiment</Text>
|
||||
</Button>
|
||||
|
||||
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
Delete Experiment
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
If you delete this experiment all the associated prompts and scenarios will be deleted
|
||||
as well. Are you sure?
|
||||
</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="red" onClick={onDeleteConfirm} ml={3}>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -6,13 +6,14 @@ import {
|
||||
DrawerHeader,
|
||||
DrawerOverlay,
|
||||
Heading,
|
||||
Stack,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import EditScenarioVars from "./EditScenarioVars";
|
||||
import EditEvaluations from "./EditEvaluations";
|
||||
import EditScenarioVars from "../OutputsTable/EditScenarioVars";
|
||||
import EditEvaluations from "../OutputsTable/EditEvaluations";
|
||||
import { useAppStore } from "~/state/store";
|
||||
import { DeleteButton } from "./DeleteButton";
|
||||
|
||||
export default function SettingsDrawer() {
|
||||
export default function ExperimentSettingsDrawer() {
|
||||
const isOpen = useAppStore((state) => state.drawerOpen);
|
||||
const closeDrawer = useAppStore((state) => state.closeDrawer);
|
||||
|
||||
@@ -22,13 +23,16 @@ export default function SettingsDrawer() {
|
||||
<DrawerContent>
|
||||
<DrawerCloseButton />
|
||||
<DrawerHeader>
|
||||
<Heading size="md">Settings</Heading>
|
||||
<Heading size="md">Experiment Settings</Heading>
|
||||
</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<Stack spacing={6}>
|
||||
<DrawerBody h="full" pb={4}>
|
||||
<VStack h="full" justifyContent="space-between">
|
||||
<VStack spacing={6}>
|
||||
<EditScenarioVars />
|
||||
<EditEvaluations />
|
||||
</Stack>
|
||||
</VStack>
|
||||
<DeleteButton />
|
||||
</VStack>
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
57
app/src/components/OutputsTable/AddVariantButton.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Box, Flex, Icon, Spinner } from "@chakra-ui/react";
|
||||
import { BsPlus } from "react-icons/bs";
|
||||
import { Text } from "@chakra-ui/react";
|
||||
import { api } from "~/utils/api";
|
||||
import {
|
||||
useExperiment,
|
||||
useExperimentAccess,
|
||||
useHandledAsyncCallback,
|
||||
useVisibleScenarioIds,
|
||||
} from "~/utils/hooks";
|
||||
import { cellPadding } from "../constants";
|
||||
import { ActionButton } from "./ScenariosHeader";
|
||||
|
||||
export default function AddVariantButton() {
|
||||
const experiment = useExperiment();
|
||||
const mutation = api.promptVariants.create.useMutation();
|
||||
const utils = api.useContext();
|
||||
const visibleScenarios = useVisibleScenarioIds();
|
||||
|
||||
const [onClick, loading] = useHandledAsyncCallback(async () => {
|
||||
if (!experiment.data) return;
|
||||
await mutation.mutateAsync({
|
||||
experimentId: experiment.data.id,
|
||||
streamScenarios: visibleScenarios,
|
||||
});
|
||||
await utils.promptVariants.list.invalidate();
|
||||
}, [mutation]);
|
||||
|
||||
const { canModify } = useExperimentAccess();
|
||||
if (!canModify) return <Box w={cellPadding.x} />;
|
||||
|
||||
return (
|
||||
<Flex w="100%" justifyContent="flex-end">
|
||||
<ActionButton
|
||||
onClick={onClick}
|
||||
py={5}
|
||||
leftIcon={<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />}
|
||||
>
|
||||
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
|
||||
</ActionButton>
|
||||
{/* <Button
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
fontWeight="normal"
|
||||
bgColor="transparent"
|
||||
_hover={{ bgColor: "gray.100" }}
|
||||
px={cellPadding.x}
|
||||
onClick={onClick}
|
||||
height="unset"
|
||||
minH={headerMinHeight}
|
||||
>
|
||||
<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />
|
||||
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
|
||||
</Button> */}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -11,14 +11,16 @@ import {
|
||||
FormLabel,
|
||||
Select,
|
||||
FormHelperText,
|
||||
Code,
|
||||
} from "@chakra-ui/react";
|
||||
import { type Evaluation, EvaluationMatchType } from "@prisma/client";
|
||||
import { type Evaluation, EvalType } from "@prisma/client";
|
||||
import { useCallback, useState } from "react";
|
||||
import { BsPencil, BsX } from "react-icons/bs";
|
||||
import { api } from "~/utils/api";
|
||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||
import AutoResizeTextArea from "../AutoResizeTextArea";
|
||||
|
||||
type EvalValues = Pick<Evaluation, "name" | "matchString" | "matchType">;
|
||||
type EvalValues = Pick<Evaluation, "label" | "value" | "evalType">;
|
||||
|
||||
export function EvaluationEditor(props: {
|
||||
evaluation: Evaluation | null;
|
||||
@@ -27,35 +29,35 @@ export function EvaluationEditor(props: {
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const [values, setValues] = useState<EvalValues>({
|
||||
name: props.evaluation?.name ?? props.defaultName ?? "",
|
||||
matchString: props.evaluation?.matchString ?? "",
|
||||
matchType: props.evaluation?.matchType ?? "CONTAINS",
|
||||
label: props.evaluation?.label ?? props.defaultName ?? "",
|
||||
value: props.evaluation?.value ?? "",
|
||||
evalType: props.evaluation?.evalType ?? "CONTAINS",
|
||||
});
|
||||
|
||||
return (
|
||||
<VStack borderTopWidth={1} borderColor="gray.200" py={4}>
|
||||
<HStack w="100%">
|
||||
<FormControl flex={1}>
|
||||
<FormLabel fontSize="sm">Evaluation Name</FormLabel>
|
||||
<FormLabel fontSize="sm">Eval Name</FormLabel>
|
||||
<Input
|
||||
size="sm"
|
||||
value={values.name}
|
||||
onChange={(e) => setValues((values) => ({ ...values, name: e.target.value }))}
|
||||
value={values.label}
|
||||
onChange={(e) => setValues((values) => ({ ...values, label: e.target.value }))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl flex={1}>
|
||||
<FormLabel fontSize="sm">Match Type</FormLabel>
|
||||
<FormLabel fontSize="sm">Eval Type</FormLabel>
|
||||
<Select
|
||||
size="sm"
|
||||
value={values.matchType}
|
||||
value={values.evalType}
|
||||
onChange={(e) =>
|
||||
setValues((values) => ({
|
||||
...values,
|
||||
matchType: e.target.value as EvaluationMatchType,
|
||||
evalType: e.target.value as EvalType,
|
||||
}))
|
||||
}
|
||||
>
|
||||
{Object.values(EvaluationMatchType).map((type) => (
|
||||
{Object.values(EvalType).map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{type}
|
||||
</option>
|
||||
@@ -63,17 +65,37 @@ export function EvaluationEditor(props: {
|
||||
</Select>
|
||||
</FormControl>
|
||||
</HStack>
|
||||
{["CONTAINS", "DOES_NOT_CONTAIN"].includes(values.evalType) && (
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Match String</FormLabel>
|
||||
<Input
|
||||
size="sm"
|
||||
value={values.matchString}
|
||||
onChange={(e) => setValues((values) => ({ ...values, matchString: e.target.value }))}
|
||||
value={values.value}
|
||||
onChange={(e) => setValues((values) => ({ ...values, value: e.target.value }))}
|
||||
/>
|
||||
<FormHelperText>
|
||||
This string will be interpreted as a regex and checked against each model output.
|
||||
This string will be interpreted as a regex and checked against each model output. You
|
||||
can include scenario variables using <Code>{"{{curly_braces}}"}</Code>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
{values.evalType === "GPT4_EVAL" && (
|
||||
<FormControl pt={2}>
|
||||
<FormLabel fontSize="sm">GPT4 Instructions</FormLabel>
|
||||
<AutoResizeTextArea
|
||||
size="sm"
|
||||
value={values.value}
|
||||
onChange={(e) => setValues((values) => ({ ...values, value: e.target.value }))}
|
||||
minRows={3}
|
||||
/>
|
||||
<FormHelperText>
|
||||
Give instructions to GPT-4 for how to evaluate your prompt. It will have access to the
|
||||
full scenario as well as the output it is evaluating. It will <strong>not</strong> have
|
||||
access to the specific prompt variant, so be sure to be clear about the task you want it
|
||||
to perform.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
<HStack alignSelf="flex-end">
|
||||
<Button size="sm" onClick={props.onCancel} colorScheme="gray">
|
||||
Cancel
|
||||
@@ -125,6 +147,7 @@ export default function EditEvaluations() {
|
||||
}
|
||||
await utils.evaluations.list.invalidate();
|
||||
await utils.promptVariants.stats.invalidate();
|
||||
await utils.scenarioVariantCells.get.invalidate();
|
||||
}, []);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
@@ -156,9 +179,9 @@ export default function EditEvaluations() {
|
||||
align="center"
|
||||
key={evaluation.id}
|
||||
>
|
||||
<Text fontWeight="bold">{evaluation.name}</Text>
|
||||
<Text fontWeight="bold">{evaluation.label}</Text>
|
||||
<Text flex={1}>
|
||||
{evaluation.matchType}: "{evaluation.matchString}"
|
||||
{evaluation.evalType}: "{evaluation.value}"
|
||||
</Text>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Text, Button, HStack, Heading, Icon, Input, Stack, Code } from "@chakra-ui/react";
|
||||
import { Text, Button, HStack, Heading, Icon, Input, Stack } from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
import { BsCheck, BsX } from "react-icons/bs";
|
||||
import { api } from "~/utils/api";
|
||||
@@ -36,8 +36,7 @@ export default function EditScenarioVars() {
|
||||
<Heading size="sm">Scenario Variables</Heading>
|
||||
<Stack spacing={2}>
|
||||
<Text fontSize="sm">
|
||||
Scenario variables can be used in your prompt variants as well as evaluations. Reference
|
||||
them using <Code>{"{{curly_braces}}"}</Code>.
|
||||
Scenario variables can be used in your prompt variants as well as evaluations.
|
||||
</Text>
|
||||
<HStack spacing={0}>
|
||||
<Input
|
||||
47
app/src/components/OutputsTable/FloatingLabelInput.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { FormLabel, FormControl, type TextareaProps } from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
import AutoResizeTextArea from "../AutoResizeTextArea";
|
||||
|
||||
export const FloatingLabelInput = ({
|
||||
label,
|
||||
value,
|
||||
...props
|
||||
}: { label: string; value: string } & TextareaProps) => {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
return (
|
||||
<FormControl position="relative">
|
||||
<FormLabel
|
||||
position="absolute"
|
||||
left="10px"
|
||||
top={isFocused || !!value ? 0 : 3}
|
||||
transform={isFocused || !!value ? "translateY(-50%)" : "translateY(0)"}
|
||||
fontSize={isFocused || !!value ? "12px" : "16px"}
|
||||
transition="all 0.15s"
|
||||
zIndex="5"
|
||||
bg="white"
|
||||
px={1}
|
||||
lineHeight="1"
|
||||
pointerEvents="none"
|
||||
color={isFocused ? "blue.500" : "gray.500"}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
<AutoResizeTextArea
|
||||
px={3}
|
||||
pt={3}
|
||||
pb={2}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
borderRadius="md"
|
||||
borderColor={isFocused ? "blue.500" : "gray.400"}
|
||||
autoComplete="off"
|
||||
value={value}
|
||||
maxHeight={32}
|
||||
overflowY="auto"
|
||||
overflowX="hidden"
|
||||
{...props}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
19
app/src/components/OutputsTable/OutputCell/CellContent.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { type StackProps, VStack } from "@chakra-ui/react";
|
||||
import { CellOptions } from "./CellOptions";
|
||||
|
||||
export const CellContent = ({
|
||||
hardRefetch,
|
||||
hardRefetching,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
hardRefetch: () => void;
|
||||
hardRefetching: boolean;
|
||||
} & StackProps) => (
|
||||
<VStack w="full" alignItems="flex-start" {...props}>
|
||||
<CellOptions refetchingOutput={hardRefetching} refetchOutput={hardRefetch} />
|
||||
<VStack w="full" alignItems="flex-start" maxH={500} overflowY="auto">
|
||||
{children}
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
37
app/src/components/OutputsTable/OutputCell/CellOptions.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Button, HStack, Icon, Spinner, Tooltip } from "@chakra-ui/react";
|
||||
import { BsArrowClockwise } from "react-icons/bs";
|
||||
import { useExperimentAccess } from "~/utils/hooks";
|
||||
|
||||
export const CellOptions = ({
|
||||
refetchingOutput,
|
||||
refetchOutput,
|
||||
}: {
|
||||
refetchingOutput: boolean;
|
||||
refetchOutput: () => void;
|
||||
}) => {
|
||||
const { canModify } = useExperimentAccess();
|
||||
return (
|
||||
<HStack justifyContent="flex-end" w="full">
|
||||
{canModify && (
|
||||
<Tooltip label="Refetch output" aria-label="refetch output">
|
||||
<Button
|
||||
size="xs"
|
||||
w={4}
|
||||
h={4}
|
||||
py={4}
|
||||
px={4}
|
||||
minW={0}
|
||||
borderRadius={8}
|
||||
color="gray.500"
|
||||
variant="ghost"
|
||||
cursor="pointer"
|
||||
onClick={refetchOutput}
|
||||
aria-label="refetch output"
|
||||
>
|
||||
<Icon as={refetchingOutput ? Spinner : BsArrowClockwise} boxSize={4} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
204
app/src/components/OutputsTable/OutputCell/OutputCell.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import { api } from "~/utils/api";
|
||||
import { type PromptVariant, type Scenario } from "../types";
|
||||
import { Text, VStack } from "@chakra-ui/react";
|
||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { docco } from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
||||
import stringify from "json-stringify-pretty-compact";
|
||||
import { type ReactElement, useState, useEffect, Fragment } from "react";
|
||||
import useSocket from "~/utils/useSocket";
|
||||
import { OutputStats } from "./OutputStats";
|
||||
import { RetryCountdown } from "./RetryCountdown";
|
||||
import frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
||||
import { ResponseLog } from "./ResponseLog";
|
||||
import { CellContent } from "./CellContent";
|
||||
|
||||
const WAITING_MESSAGE_INTERVAL = 20000;
|
||||
|
||||
export default function OutputCell({
|
||||
scenario,
|
||||
variant,
|
||||
}: {
|
||||
scenario: Scenario;
|
||||
variant: PromptVariant;
|
||||
}): ReactElement | null {
|
||||
const utils = api.useContext();
|
||||
const experiment = useExperiment();
|
||||
const vars = api.templateVars.list.useQuery({
|
||||
experimentId: experiment.data?.id ?? "",
|
||||
}).data;
|
||||
|
||||
const scenarioVariables = scenario.variableValues as Record<string, string>;
|
||||
const templateHasVariables =
|
||||
vars?.length === 0 || vars?.some((v) => scenarioVariables[v.label] !== undefined);
|
||||
|
||||
let disabledReason: string | null = null;
|
||||
|
||||
if (!templateHasVariables) disabledReason = "Add a value to the scenario variables to see output";
|
||||
|
||||
const [refetchInterval, setRefetchInterval] = useState(0);
|
||||
const { data: cell, isLoading: queryLoading } = api.scenarioVariantCells.get.useQuery(
|
||||
{ scenarioId: scenario.id, variantId: variant.id },
|
||||
{ refetchInterval },
|
||||
);
|
||||
|
||||
const provider =
|
||||
frontendModelProviders[variant.modelProvider as keyof typeof frontendModelProviders];
|
||||
|
||||
type OutputSchema = Parameters<typeof provider.normalizeOutput>[0];
|
||||
|
||||
const { mutateAsync: hardRefetchMutate } = api.scenarioVariantCells.forceRefetch.useMutation();
|
||||
const [hardRefetch, hardRefetching] = useHandledAsyncCallback(async () => {
|
||||
await hardRefetchMutate({ scenarioId: scenario.id, variantId: variant.id });
|
||||
await utils.scenarioVariantCells.get.invalidate({
|
||||
scenarioId: scenario.id,
|
||||
variantId: variant.id,
|
||||
});
|
||||
await utils.promptVariants.stats.invalidate({
|
||||
variantId: variant.id,
|
||||
});
|
||||
}, [hardRefetchMutate, scenario.id, variant.id]);
|
||||
|
||||
const fetchingOutput = queryLoading || hardRefetching;
|
||||
|
||||
const awaitingOutput =
|
||||
!cell ||
|
||||
!cell.evalsComplete ||
|
||||
cell.retrievalStatus === "PENDING" ||
|
||||
cell.retrievalStatus === "IN_PROGRESS" ||
|
||||
hardRefetching;
|
||||
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : 0), [awaitingOutput]);
|
||||
|
||||
// TODO: disconnect from socket if we're not streaming anymore
|
||||
const streamedMessage = useSocket<OutputSchema>(cell?.id);
|
||||
|
||||
if (!vars) return null;
|
||||
|
||||
if (!cell && !fetchingOutput)
|
||||
return (
|
||||
<CellContent hardRefetching={hardRefetching} hardRefetch={hardRefetch}>
|
||||
<Text color="gray.500">Error retrieving output</Text>
|
||||
</CellContent>
|
||||
);
|
||||
|
||||
if (cell && cell.errorMessage) {
|
||||
return (
|
||||
<CellContent hardRefetching={hardRefetching} hardRefetch={hardRefetch}>
|
||||
<Text color="red.500">{cell.errorMessage}</Text>
|
||||
</CellContent>
|
||||
);
|
||||
}
|
||||
|
||||
if (disabledReason) return <Text color="gray.500">{disabledReason}</Text>;
|
||||
|
||||
const mostRecentResponse = cell?.modelResponses[cell.modelResponses.length - 1];
|
||||
const showLogs = !streamedMessage && !mostRecentResponse?.output;
|
||||
|
||||
if (showLogs)
|
||||
return (
|
||||
<CellContent
|
||||
hardRefetching={hardRefetching}
|
||||
hardRefetch={hardRefetch}
|
||||
alignItems="flex-start"
|
||||
fontFamily="inconsolata, monospace"
|
||||
spacing={0}
|
||||
>
|
||||
{cell?.jobQueuedAt && <ResponseLog time={cell.jobQueuedAt} title="Job queued" />}
|
||||
{cell?.jobStartedAt && <ResponseLog time={cell.jobStartedAt} title="Job started" />}
|
||||
{cell?.modelResponses?.map((response) => {
|
||||
let numWaitingMessages = 0;
|
||||
const relativeWaitingTime = response.receivedAt
|
||||
? response.receivedAt.getTime()
|
||||
: Date.now();
|
||||
if (response.requestedAt) {
|
||||
numWaitingMessages = Math.floor(
|
||||
(relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Fragment key={response.id}>
|
||||
{response.requestedAt && (
|
||||
<ResponseLog time={response.requestedAt} title="Request sent to API" />
|
||||
)}
|
||||
{response.requestedAt &&
|
||||
Array.from({ length: numWaitingMessages }, (_, i) => (
|
||||
<ResponseLog
|
||||
key={`waiting-${i}`}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
time={new Date(response.requestedAt!.getTime() + i * WAITING_MESSAGE_INTERVAL)}
|
||||
title="Waiting for response"
|
||||
/>
|
||||
))}
|
||||
{response.receivedAt && (
|
||||
<ResponseLog
|
||||
time={response.receivedAt}
|
||||
title="Response received from API"
|
||||
message={`statusCode: ${response.statusCode ?? ""}\n ${
|
||||
response.errorMessage ?? ""
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}) ?? null}
|
||||
{mostRecentResponse?.retryTime && (
|
||||
<RetryCountdown retryTime={mostRecentResponse.retryTime} />
|
||||
)}
|
||||
</CellContent>
|
||||
);
|
||||
|
||||
const normalizedOutput = mostRecentResponse?.output
|
||||
? provider.normalizeOutput(mostRecentResponse?.output)
|
||||
: streamedMessage
|
||||
? provider.normalizeOutput(streamedMessage)
|
||||
: null;
|
||||
|
||||
if (mostRecentResponse?.output && normalizedOutput?.type === "json") {
|
||||
return (
|
||||
<VStack
|
||||
w="100%"
|
||||
h="100%"
|
||||
fontSize="xs"
|
||||
flexWrap="wrap"
|
||||
overflowX="hidden"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<CellContent
|
||||
hardRefetching={hardRefetching}
|
||||
hardRefetch={hardRefetch}
|
||||
w="full"
|
||||
flex={1}
|
||||
spacing={0}
|
||||
>
|
||||
<SyntaxHighlighter
|
||||
customStyle={{ overflowX: "unset", width: "100%", flex: 1 }}
|
||||
language="json"
|
||||
style={docco}
|
||||
lineProps={{
|
||||
style: { wordBreak: "break-all", whiteSpace: "pre-wrap" },
|
||||
}}
|
||||
wrapLines
|
||||
>
|
||||
{stringify(normalizedOutput.value, { maxLength: 40 })}
|
||||
</SyntaxHighlighter>
|
||||
</CellContent>
|
||||
<OutputStats modelResponse={mostRecentResponse} scenario={scenario} />
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
const contentToDisplay = (normalizedOutput?.type === "text" && normalizedOutput.value) || "";
|
||||
|
||||
return (
|
||||
<VStack w="100%" h="100%" justifyContent="space-between" whiteSpace="pre-wrap">
|
||||
<VStack w="full" alignItems="flex-start" spacing={0}>
|
||||
<CellContent hardRefetching={hardRefetching} hardRefetch={hardRefetch}>
|
||||
<Text>{contentToDisplay}</Text>
|
||||
</CellContent>
|
||||
</VStack>
|
||||
{mostRecentResponse?.output && (
|
||||
<OutputStats modelResponse={mostRecentResponse} scenario={scenario} />
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
69
app/src/components/OutputsTable/OutputCell/OutputStats.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { type Scenario } from "../types";
|
||||
import { type RouterOutputs } from "~/utils/api";
|
||||
import { HStack, Icon, Text, Tooltip } from "@chakra-ui/react";
|
||||
import { BsCheck, BsClock, BsCurrencyDollar, BsX } from "react-icons/bs";
|
||||
import { CostTooltip } from "~/components/tooltip/CostTooltip";
|
||||
|
||||
const SHOW_TIME = true;
|
||||
|
||||
export const OutputStats = ({
|
||||
modelResponse,
|
||||
}: {
|
||||
modelResponse: NonNullable<
|
||||
NonNullable<RouterOutputs["scenarioVariantCells"]["get"]>["modelResponses"][0]
|
||||
>;
|
||||
scenario: Scenario;
|
||||
}) => {
|
||||
const timeToComplete =
|
||||
modelResponse.receivedAt && modelResponse.requestedAt
|
||||
? modelResponse.receivedAt.getTime() - modelResponse.requestedAt.getTime()
|
||||
: 0;
|
||||
|
||||
const promptTokens = modelResponse.promptTokens;
|
||||
const completionTokens = modelResponse.completionTokens;
|
||||
|
||||
return (
|
||||
<HStack w="full" align="center" color="gray.500" fontSize="2xs" mt={{ base: 0, md: 1 }}>
|
||||
<HStack flex={1}>
|
||||
{modelResponse.outputEvaluations.map((evaluation) => {
|
||||
const passed = evaluation.result > 0.5;
|
||||
return (
|
||||
<Tooltip
|
||||
isDisabled={!evaluation.details}
|
||||
label={evaluation.details}
|
||||
key={evaluation.id}
|
||||
shouldWrapChildren
|
||||
>
|
||||
<HStack spacing={0}>
|
||||
<Text>{evaluation.evaluation.label}</Text>
|
||||
<Icon
|
||||
as={passed ? BsCheck : BsX}
|
||||
color={passed ? "green.500" : "red.500"}
|
||||
boxSize={6}
|
||||
/>
|
||||
</HStack>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</HStack>
|
||||
{modelResponse.cost && (
|
||||
<CostTooltip
|
||||
promptTokens={promptTokens}
|
||||
completionTokens={completionTokens}
|
||||
cost={modelResponse.cost}
|
||||
>
|
||||
<HStack spacing={0}>
|
||||
<Icon as={BsCurrencyDollar} />
|
||||
<Text mr={1}>{modelResponse.cost.toFixed(3)}</Text>
|
||||
</HStack>
|
||||
</CostTooltip>
|
||||
)}
|
||||
{SHOW_TIME && (
|
||||
<HStack spacing={0.5}>
|
||||
<Icon as={BsClock} />
|
||||
<Text>{(timeToComplete / 1000).toFixed(2)}s</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
22
app/src/components/OutputsTable/OutputCell/ResponseLog.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { HStack, VStack, Text } from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const ResponseLog = ({
|
||||
time,
|
||||
title,
|
||||
message,
|
||||
}: {
|
||||
time: Date;
|
||||
title: string;
|
||||
message?: string;
|
||||
}) => {
|
||||
return (
|
||||
<VStack spacing={0} alignItems="flex-start">
|
||||
<HStack>
|
||||
<Text>{dayjs(time).format("HH:mm:ss")}</Text>
|
||||
<Text>{title}</Text>
|
||||
</HStack>
|
||||
{message && <Text pl={4}>{message}</Text>}
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +1,12 @@
|
||||
import { type ScenarioVariantCell } from "@prisma/client";
|
||||
import { VStack, Text } from "@chakra-ui/react";
|
||||
import { Text } from "@chakra-ui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import pluralize from "pluralize";
|
||||
|
||||
export const ErrorHandler = ({
|
||||
cell,
|
||||
refetchOutput,
|
||||
}: {
|
||||
cell: ScenarioVariantCell;
|
||||
refetchOutput: () => void;
|
||||
}) => {
|
||||
export const RetryCountdown = ({ retryTime }: { retryTime: Date }) => {
|
||||
const [msToWait, setMsToWait] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cell.retryTime) return;
|
||||
|
||||
const initialWaitTime = cell.retryTime.getTime() - Date.now();
|
||||
const initialWaitTime = retryTime.getTime() - Date.now();
|
||||
const msModuloOneSecond = initialWaitTime % 1000;
|
||||
let remainingTime = initialWaitTime - msModuloOneSecond;
|
||||
setMsToWait(remainingTime);
|
||||
@@ -36,18 +27,13 @@ export const ErrorHandler = ({
|
||||
clearInterval(interval);
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [cell.retryTime, cell.statusCode, setMsToWait, refetchOutput]);
|
||||
}, [retryTime]);
|
||||
|
||||
if (msToWait <= 0) return null;
|
||||
|
||||
return (
|
||||
<VStack w="full">
|
||||
<Text color="red.600" wordBreak="break-word">
|
||||
{cell.errorMessage}
|
||||
</Text>
|
||||
{msToWait > 0 && (
|
||||
<Text color="red.600" fontSize="sm">
|
||||
Retrying in {pluralize("second", Math.ceil(msToWait / 1000), true)}...
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +1,15 @@
|
||||
import { type DragEvent } from "react";
|
||||
import { api } from "~/utils/api";
|
||||
import { isEqual } from "lodash";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { type Scenario } from "./types";
|
||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||
import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Box, Button, Flex, HStack, Icon, Spinner, Stack, Tooltip, VStack } from "@chakra-ui/react";
|
||||
import { cellPadding } from "../constants";
|
||||
import { BsX } from "react-icons/bs";
|
||||
import { RiDraggable } from "react-icons/ri";
|
||||
import AutoResizeTextArea from "../AutoResizeTextArea";
|
||||
import { FloatingLabelInput } from "./FloatingLabelInput";
|
||||
|
||||
export default function ScenarioEditor({
|
||||
scenario,
|
||||
@@ -19,6 +19,8 @@ export default function ScenarioEditor({
|
||||
hovered: boolean;
|
||||
canHide: boolean;
|
||||
}) {
|
||||
const { canModify } = useExperimentAccess();
|
||||
|
||||
const savedValues = scenario.variableValues as Record<string, string>;
|
||||
const utils = api.useContext();
|
||||
const [isDragTarget, setIsDragTarget] = useState(false);
|
||||
@@ -72,8 +74,9 @@ export default function ScenarioEditor({
|
||||
return (
|
||||
<HStack
|
||||
alignItems="flex-start"
|
||||
pr={cellPadding.x}
|
||||
px={cellPadding.x}
|
||||
py={cellPadding.y}
|
||||
spacing={0}
|
||||
height="100%"
|
||||
draggable={!variableInputHovered}
|
||||
onDragStart={(e) => {
|
||||
@@ -93,9 +96,13 @@ export default function ScenarioEditor({
|
||||
onDrop={onReorder}
|
||||
backgroundColor={isDragTarget ? "gray.100" : "transparent"}
|
||||
>
|
||||
<Stack alignSelf="flex-start" opacity={props.hovered ? 1 : 0} spacing={0}>
|
||||
{props.canHide && (
|
||||
<>
|
||||
{canModify && props.canHide && (
|
||||
<Stack
|
||||
alignSelf="flex-start"
|
||||
opacity={props.hovered ? 1 : 0}
|
||||
spacing={0}
|
||||
ml={-cellPadding.x}
|
||||
>
|
||||
<Tooltip label="Hide scenario" hasArrow>
|
||||
{/* for some reason the tooltip can't position itself properly relative to the icon without the wrapping box */}
|
||||
<Button
|
||||
@@ -110,7 +117,7 @@ export default function ScenarioEditor({
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<Icon as={hidingInProgress ? Spinner : BsX} boxSize={6} />
|
||||
<Icon as={hidingInProgress ? Spinner : BsX} boxSize={hidingInProgress ? 4 : 6} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Icon
|
||||
@@ -119,13 +126,13 @@ export default function ScenarioEditor({
|
||||
color="gray.400"
|
||||
_hover={{ color: "gray.800", cursor: "pointer" }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{variableLabels.length === 0 ? (
|
||||
<Box color="gray.500">{vars.data ? "No scenario variables configured" : "Loading..."}</Box>
|
||||
) : (
|
||||
<VStack spacing={1}>
|
||||
<VStack spacing={4} flex={1} py={2}>
|
||||
{variableLabels.map((key) => {
|
||||
const value = values[key] ?? "";
|
||||
const layoutDirection = value.length > 20 ? "column" : "row";
|
||||
@@ -137,29 +144,14 @@ export default function ScenarioEditor({
|
||||
flexWrap="wrap"
|
||||
width="full"
|
||||
>
|
||||
<Box
|
||||
bgColor="blue.100"
|
||||
color="blue.600"
|
||||
px={1}
|
||||
my="3px"
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{key}
|
||||
</Box>
|
||||
<AutoResizeTextArea
|
||||
px={2}
|
||||
py={1}
|
||||
placeholder="empty"
|
||||
borderRadius="sm"
|
||||
fontSize="sm"
|
||||
lineHeight={1.2}
|
||||
<FloatingLabelInput
|
||||
label={key}
|
||||
isDisabled={!canModify}
|
||||
style={{ width: "100%" }}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValues((prev) => ({ ...prev, [key]: e.target.value }));
|
||||
}}
|
||||
maxH="32"
|
||||
overflowY="auto"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
@@ -167,12 +159,6 @@ export default function ScenarioEditor({
|
||||
onSave();
|
||||
}
|
||||
}}
|
||||
resize="none"
|
||||
overflow="hidden"
|
||||
flex={layoutDirection === "row" ? 1 : undefined}
|
||||
borderColor={hasChanged ? "blue.300" : "transparent"}
|
||||
_hover={{ borderColor: "gray.300" }}
|
||||
_focus={{ borderColor: "blue.500", outline: "none", bg: "white" }}
|
||||
onMouseEnter={() => setVariableInputHovered(true)}
|
||||
onMouseLeave={() => setVariableInputHovered(false)}
|
||||
/>
|
||||
74
app/src/components/OutputsTable/ScenarioPaginator.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Box, HStack, IconButton } from "@chakra-ui/react";
|
||||
import {
|
||||
BsChevronDoubleLeft,
|
||||
BsChevronDoubleRight,
|
||||
BsChevronLeft,
|
||||
BsChevronRight,
|
||||
} from "react-icons/bs";
|
||||
import { usePage, useScenarios } from "~/utils/hooks";
|
||||
|
||||
const ScenarioPaginator = () => {
|
||||
const [page, setPage] = usePage();
|
||||
const { data } = useScenarios();
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const { scenarios, startIndex, lastPage, count } = data;
|
||||
|
||||
const nextPage = () => {
|
||||
if (page < lastPage) {
|
||||
setPage(page + 1, "replace");
|
||||
}
|
||||
};
|
||||
|
||||
const prevPage = () => {
|
||||
if (page > 1) {
|
||||
setPage(page - 1, "replace");
|
||||
}
|
||||
};
|
||||
|
||||
const goToLastPage = () => setPage(lastPage, "replace");
|
||||
const goToFirstPage = () => setPage(1, "replace");
|
||||
|
||||
return (
|
||||
<HStack pt={4}>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={goToFirstPage}
|
||||
isDisabled={page === 1}
|
||||
aria-label="Go to first page"
|
||||
icon={<BsChevronDoubleLeft />}
|
||||
/>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={prevPage}
|
||||
isDisabled={page === 1}
|
||||
aria-label="Previous page"
|
||||
icon={<BsChevronLeft />}
|
||||
/>
|
||||
<Box>
|
||||
{startIndex}-{startIndex + scenarios.length - 1} / {count}
|
||||
</Box>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={nextPage}
|
||||
isDisabled={page === lastPage}
|
||||
aria-label="Next page"
|
||||
icon={<BsChevronRight />}
|
||||
/>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={goToLastPage}
|
||||
isDisabled={page === lastPage}
|
||||
aria-label="Go to last page"
|
||||
icon={<BsChevronDoubleRight />}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScenarioPaginator;
|
||||
@@ -4,11 +4,13 @@ import { cellPadding } from "../constants";
|
||||
import OutputCell from "./OutputCell/OutputCell";
|
||||
import ScenarioEditor from "./ScenarioEditor";
|
||||
import type { PromptVariant, Scenario } from "./types";
|
||||
import { borders } from "./styles";
|
||||
|
||||
const ScenarioRow = (props: {
|
||||
scenario: Scenario;
|
||||
variants: PromptVariant[];
|
||||
canHide: boolean;
|
||||
rowStart: number;
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
@@ -21,15 +23,21 @@ const ScenarioRow = (props: {
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
sx={isHovered ? highlightStyle : undefined}
|
||||
borderLeftWidth={1}
|
||||
{...borders}
|
||||
rowStart={props.rowStart}
|
||||
colStart={1}
|
||||
>
|
||||
<ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} />
|
||||
</GridItem>
|
||||
{props.variants.map((variant) => (
|
||||
{props.variants.map((variant, i) => (
|
||||
<GridItem
|
||||
key={variant.id}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
sx={isHovered ? highlightStyle : undefined}
|
||||
rowStart={props.rowStart}
|
||||
colStart={i + 2}
|
||||
{...borders}
|
||||
>
|
||||
<Box h="100%" w="100%" px={cellPadding.x} py={cellPadding.y}>
|
||||
<OutputCell key={variant.id} scenario={props.scenario} variant={variant} />
|
||||
82
app/src/components/OutputsTable/ScenariosHeader.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
Button,
|
||||
type ButtonProps,
|
||||
HStack,
|
||||
Text,
|
||||
Icon,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
IconButton,
|
||||
Spinner,
|
||||
} from "@chakra-ui/react";
|
||||
import { cellPadding } from "../constants";
|
||||
import {
|
||||
useExperiment,
|
||||
useExperimentAccess,
|
||||
useHandledAsyncCallback,
|
||||
useScenarios,
|
||||
} from "~/utils/hooks";
|
||||
import { BsGear, BsPencil, BsPlus, BsStars } from "react-icons/bs";
|
||||
import { useAppStore } from "~/state/store";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export const ActionButton = (props: ButtonProps) => (
|
||||
<Button size="sm" variant="ghost" color="gray.600" {...props} />
|
||||
);
|
||||
|
||||
export const ScenariosHeader = () => {
|
||||
const openDrawer = useAppStore((s) => s.openDrawer);
|
||||
const { canModify } = useExperimentAccess();
|
||||
const scenarios = useScenarios();
|
||||
|
||||
const experiment = useExperiment();
|
||||
const createScenarioMutation = api.scenarios.create.useMutation();
|
||||
const utils = api.useContext();
|
||||
|
||||
const [onAddScenario, loading] = useHandledAsyncCallback(
|
||||
async (autogenerate: boolean) => {
|
||||
if (!experiment.data) return;
|
||||
await createScenarioMutation.mutateAsync({
|
||||
experimentId: experiment.data.id,
|
||||
autogenerate,
|
||||
});
|
||||
await utils.scenarios.list.invalidate();
|
||||
},
|
||||
[createScenarioMutation],
|
||||
);
|
||||
|
||||
return (
|
||||
<HStack w="100%" pb={cellPadding.y} pt={0} align="center" spacing={0}>
|
||||
<Text fontSize={16} fontWeight="bold">
|
||||
Scenarios ({scenarios.data?.count})
|
||||
</Text>
|
||||
{canModify && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
mt={1}
|
||||
variant="ghost"
|
||||
aria-label="Edit Scenarios"
|
||||
icon={<Icon as={loading ? Spinner : BsGear} />}
|
||||
/>
|
||||
<MenuList fontSize="md" zIndex="dropdown" mt={-3}>
|
||||
<MenuItem
|
||||
icon={<Icon as={BsPlus} boxSize={6} mx="-5px" />}
|
||||
onClick={() => onAddScenario(false)}
|
||||
>
|
||||
Add Scenario
|
||||
</MenuItem>
|
||||
<MenuItem icon={<BsStars />} onClick={() => onAddScenario(true)}>
|
||||
Autogenerate Scenario
|
||||
</MenuItem>
|
||||
<MenuItem icon={<BsPencil />} onClick={openDrawer}>
|
||||
Edit Vars
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +1,52 @@
|
||||
import { Box, Button, HStack, Tooltip, VStack, useToast } from "@chakra-ui/react";
|
||||
import { useRef, useEffect, useState, useCallback } from "react";
|
||||
import { useHandledAsyncCallback, useModifierKeyLabel } from "~/utils/hooks";
|
||||
import { type PromptVariant } from "./types";
|
||||
import { api } from "~/utils/api";
|
||||
import { useAppStore } from "~/state/store";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
HStack,
|
||||
IconButton,
|
||||
Spinner,
|
||||
Text,
|
||||
Tooltip,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { FiMaximize, FiMinimize } from "react-icons/fi";
|
||||
import { editorBackground } from "~/state/sharedVariantEditor.slice";
|
||||
import { useAppStore } from "~/state/store";
|
||||
import { api } from "~/utils/api";
|
||||
import {
|
||||
useExperimentAccess,
|
||||
useHandledAsyncCallback,
|
||||
useModifierKeyLabel,
|
||||
useVisibleScenarioIds,
|
||||
} from "~/utils/hooks";
|
||||
import { type PromptVariant } from "./types";
|
||||
|
||||
export default function VariantEditor(props: { variant: PromptVariant }) {
|
||||
const { canModify } = useExperimentAccess();
|
||||
const monaco = useAppStore.use.sharedVariantEditor.monaco();
|
||||
const editorRef = useRef<ReturnType<NonNullable<typeof monaco>["editor"]["create"]> | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [editorId] = useState(() => `editor_${Math.random().toString(36).substring(7)}`);
|
||||
const [isChanged, setIsChanged] = useState(false);
|
||||
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
setIsFullscreen((prev) => !prev);
|
||||
editorRef.current?.focus();
|
||||
}, [setIsFullscreen]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEsc = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isFullscreen) {
|
||||
toggleFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleEsc);
|
||||
return () => window.removeEventListener("keydown", handleEsc);
|
||||
}, [isFullscreen, toggleFullscreen]);
|
||||
|
||||
const lastSavedFn = props.variant.constructFn;
|
||||
|
||||
const modifierKey = useModifierKeyLabel();
|
||||
@@ -22,13 +57,20 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
||||
setIsChanged(currentFn.length > 0 && currentFn !== lastSavedFn);
|
||||
}, [lastSavedFn]);
|
||||
|
||||
useEffect(checkForChanges, [checkForChanges, lastSavedFn]);
|
||||
const matchUpdatedSavedFn = useCallback(() => {
|
||||
if (!editorRef.current) return;
|
||||
editorRef.current.setValue(lastSavedFn);
|
||||
setIsChanged(false);
|
||||
}, [lastSavedFn]);
|
||||
|
||||
useEffect(matchUpdatedSavedFn, [matchUpdatedSavedFn, lastSavedFn]);
|
||||
|
||||
const replaceVariant = api.promptVariants.replaceVariant.useMutation();
|
||||
const utils = api.useContext();
|
||||
const toast = useToast();
|
||||
const visibleScenarios = useVisibleScenarioIds();
|
||||
|
||||
const [onSave] = useHandledAsyncCallback(async () => {
|
||||
const [onSave, saveInProgress] = useHandledAsyncCallback(async () => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
await editorRef.current.getAction("editor.action.formatDocument")?.run();
|
||||
@@ -41,26 +83,12 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
||||
const model = editorRef.current.getModel();
|
||||
if (!model) return;
|
||||
|
||||
const markers = monaco?.editor.getModelMarkers({ resource: model.uri });
|
||||
const hasErrors = markers?.some((m) => m.severity === monaco?.MarkerSeverity.Error);
|
||||
|
||||
if (hasErrors) {
|
||||
toast({
|
||||
title: "Invalid TypeScript",
|
||||
description: "Please fix the TypeScript errors before saving.",
|
||||
status: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the user defined the prompt with the string "prompt\w*=" somewhere
|
||||
const promptRegex = /prompt\s*=/;
|
||||
const promptRegex = /definePrompt\(/;
|
||||
if (!promptRegex.test(currentFn)) {
|
||||
console.log("no prompt");
|
||||
console.log(currentFn);
|
||||
toast({
|
||||
title: "Missing prompt",
|
||||
description: "Please define the prompt (eg. `prompt = { ...`).",
|
||||
description: "Please define the prompt (eg. `definePrompt(...`",
|
||||
status: "error",
|
||||
});
|
||||
return;
|
||||
@@ -69,6 +97,7 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
||||
const resp = await replaceVariant.mutateAsync({
|
||||
id: props.variant.id,
|
||||
constructFn: currentFn,
|
||||
streamScenarios: visibleScenarios,
|
||||
});
|
||||
if (resp.status === "error") {
|
||||
return toast({
|
||||
@@ -104,13 +133,26 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
||||
wordWrapBreakAfterCharacters: "",
|
||||
wordWrapBreakBeforeCharacters: "",
|
||||
quickSuggestions: true,
|
||||
readOnly: !canModify,
|
||||
});
|
||||
|
||||
editorRef.current.onDidFocusEditorText(() => {
|
||||
// Workaround because otherwise the command only works on whatever
|
||||
// Workaround because otherwise the commands only work on whatever
|
||||
// editor was loaded on the page last.
|
||||
// https://github.com/microsoft/monaco-editor/issues/2947#issuecomment-1422265201
|
||||
editorRef.current?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, onSave);
|
||||
editorRef.current.onDidFocusEditorText(() => {
|
||||
editorRef.current?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, onSave);
|
||||
|
||||
editorRef.current?.addCommand(
|
||||
monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF,
|
||||
toggleFullscreen,
|
||||
);
|
||||
|
||||
// Exit fullscreen with escape
|
||||
editorRef.current?.addCommand(monaco.KeyCode.Escape, () => {
|
||||
if (isFullscreen) {
|
||||
toggleFullscreen();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
editorRef.current.onDidChangeModelContent(checkForChanges);
|
||||
@@ -131,21 +173,48 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
}, [monaco, editorId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
editorRef.current.updateOptions({
|
||||
readOnly: !canModify,
|
||||
});
|
||||
}, [canModify]);
|
||||
|
||||
return (
|
||||
<Box w="100%" pos="relative">
|
||||
<VStack
|
||||
spacing={0}
|
||||
align="stretch"
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
color="gray.600"
|
||||
py={2}
|
||||
<Box
|
||||
w="100%"
|
||||
ref={containerRef}
|
||||
sx={
|
||||
isFullscreen
|
||||
? {
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}
|
||||
: { h: "400px", w: "100%" }
|
||||
}
|
||||
bgColor={editorBackground}
|
||||
zIndex={isFullscreen ? 1000 : "unset"}
|
||||
pos="relative"
|
||||
_hover={{ ".fullscreen-toggle": { opacity: 1 } }}
|
||||
>
|
||||
<code>{`function constructPrompt(scenario: Scenario): Prompt {`}</code>
|
||||
<div id={editorId} style={{ height: "300px", width: "100%" }}></div>
|
||||
<code>{`return prompt; }`}</code>
|
||||
</VStack>
|
||||
<Box id={editorId} w="100%" h="100%" />
|
||||
<Tooltip label={`${modifierKey} + ⇧ + F`}>
|
||||
<IconButton
|
||||
className="fullscreen-toggle"
|
||||
aria-label="Minimize"
|
||||
icon={isFullscreen ? <FiMinimize /> : <FiMaximize />}
|
||||
position="absolute"
|
||||
top={2}
|
||||
right={2}
|
||||
onClick={toggleFullscreen}
|
||||
opacity={0}
|
||||
transition="opacity 0.2s"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{isChanged && (
|
||||
<HStack pos="absolute" bottom={2} right={2}>
|
||||
<Button
|
||||
@@ -158,9 +227,9 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Tooltip label={`${modifierKey} + Enter`}>
|
||||
<Button size="sm" onClick={onSave} colorScheme="blue">
|
||||
Save
|
||||
<Tooltip label={`${modifierKey} + S`}>
|
||||
<Button size="sm" onClick={onSave} colorScheme="blue" w={16} disabled={saveInProgress}>
|
||||
{saveInProgress ? <Spinner boxSize={4} /> : <Text>Save</Text>}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
@@ -5,8 +5,10 @@ import { api } from "~/utils/api";
|
||||
import chroma from "chroma-js";
|
||||
import { BsCurrencyDollar } from "react-icons/bs";
|
||||
import { CostTooltip } from "../tooltip/CostTooltip";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function VariantStats(props: { variant: PromptVariant }) {
|
||||
const [refetchInterval, setRefetchInterval] = useState(0);
|
||||
const { data } = api.promptVariants.stats.useQuery(
|
||||
{
|
||||
variantId: props.variant.id,
|
||||
@@ -19,10 +21,15 @@ export default function VariantStats(props: { variant: PromptVariant }) {
|
||||
completionTokens: 0,
|
||||
scenarioCount: 0,
|
||||
outputCount: 0,
|
||||
awaitingEvals: false,
|
||||
},
|
||||
refetchInterval,
|
||||
},
|
||||
);
|
||||
|
||||
// Poll every two seconds while we are waiting for LLM retrievals to finish
|
||||
useEffect(() => setRefetchInterval(data.awaitingEvals ? 5000 : 0), [data.awaitingEvals]);
|
||||
|
||||
const [passColor, neutralColor, failColor] = useToken("colors", [
|
||||
"green.500",
|
||||
"gray.500",
|
||||
@@ -33,21 +40,25 @@ export default function VariantStats(props: { variant: PromptVariant }) {
|
||||
|
||||
const showNumFinished = data.scenarioCount > 0 && data.scenarioCount !== data.outputCount;
|
||||
|
||||
if (!(data.evalResults.length > 0) && !data.overallCost) return null;
|
||||
|
||||
return (
|
||||
<HStack justifyContent="space-between" alignItems="center" mx="2" fontSize="xs">
|
||||
<HStack
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mx="2"
|
||||
fontSize="xs"
|
||||
py={cellPadding.y}
|
||||
>
|
||||
{showNumFinished && (
|
||||
<Text>
|
||||
{data.outputCount} / {data.scenarioCount}
|
||||
</Text>
|
||||
)}
|
||||
<HStack px={cellPadding.x} py={cellPadding.y}>
|
||||
<HStack px={cellPadding.x}>
|
||||
{data.evalResults.map((result) => {
|
||||
const passedFrac = result.passCount / (result.passCount + result.failCount);
|
||||
const passedFrac = result.passCount / result.totalCount;
|
||||
return (
|
||||
<HStack key={result.id}>
|
||||
<Text>{result.evaluation.name}</Text>
|
||||
<Text>{result.label}</Text>
|
||||
<Text color={scale(passedFrac).hex()} fontWeight="bold">
|
||||
{(passedFrac * 100).toFixed(1)}%
|
||||
</Text>
|
||||
@@ -61,7 +72,7 @@ export default function VariantStats(props: { variant: PromptVariant }) {
|
||||
completionTokens={data.completionTokens}
|
||||
cost={data.overallCost}
|
||||
>
|
||||
<HStack spacing={0} align="center" color="gray.500" my="2">
|
||||
<HStack spacing={0} align="center" color="gray.500">
|
||||
<Icon as={BsCurrencyDollar} />
|
||||
<Text mr={1}>{data.overallCost.toFixed(3)}</Text>
|
||||
</HStack>
|
||||
107
app/src/components/OutputsTable/index.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Grid, GridItem, type GridItemProps } from "@chakra-ui/react";
|
||||
import { api } from "~/utils/api";
|
||||
import AddVariantButton from "./AddVariantButton";
|
||||
import ScenarioRow from "./ScenarioRow";
|
||||
import VariantEditor from "./VariantEditor";
|
||||
import VariantHeader from "../VariantHeader/VariantHeader";
|
||||
import VariantStats from "./VariantStats";
|
||||
import { ScenariosHeader } from "./ScenariosHeader";
|
||||
import { borders } from "./styles";
|
||||
import { useScenarios } from "~/utils/hooks";
|
||||
import ScenarioPaginator from "./ScenarioPaginator";
|
||||
import { Fragment } from "react";
|
||||
|
||||
export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) {
|
||||
const variants = api.promptVariants.list.useQuery(
|
||||
{ experimentId: experimentId as string },
|
||||
{ enabled: !!experimentId },
|
||||
);
|
||||
|
||||
const scenarios = useScenarios();
|
||||
|
||||
if (!variants.data || !scenarios.data) return null;
|
||||
|
||||
const allCols = variants.data.length + 2;
|
||||
const variantHeaderRows = 3;
|
||||
const scenarioHeaderRows = 1;
|
||||
const scenarioFooterRows = 1;
|
||||
const visibleScenariosCount = scenarios.data.scenarios.length;
|
||||
const allRows =
|
||||
variantHeaderRows + scenarioHeaderRows + visibleScenariosCount + scenarioFooterRows;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
pt={4}
|
||||
pb={24}
|
||||
pl={8}
|
||||
display="grid"
|
||||
gridTemplateColumns={`250px repeat(${variants.data.length}, minmax(300px, 1fr)) auto`}
|
||||
sx={{
|
||||
"> *": {
|
||||
borderColor: "gray.300",
|
||||
},
|
||||
}}
|
||||
fontSize="sm"
|
||||
>
|
||||
<GridItem rowSpan={variantHeaderRows}>
|
||||
<AddVariantButton />
|
||||
</GridItem>
|
||||
|
||||
{variants.data.map((variant, i) => {
|
||||
const sharedProps: GridItemProps = {
|
||||
...borders,
|
||||
colStart: i + 2,
|
||||
borderLeftWidth: i === 0 ? 1 : 0,
|
||||
marginLeft: i === 0 ? "-1px" : 0,
|
||||
backgroundColor: "gray.100",
|
||||
};
|
||||
return (
|
||||
<Fragment key={variant.uiId}>
|
||||
<VariantHeader
|
||||
variant={variant}
|
||||
canHide={variants.data.length > 1}
|
||||
rowStart={1}
|
||||
{...sharedProps}
|
||||
/>
|
||||
<GridItem rowStart={2} {...sharedProps}>
|
||||
<VariantEditor variant={variant} />
|
||||
</GridItem>
|
||||
<GridItem rowStart={3} {...sharedProps}>
|
||||
<VariantStats variant={variant} />
|
||||
</GridItem>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
<GridItem
|
||||
colSpan={allCols - 1}
|
||||
rowStart={variantHeaderRows + 1}
|
||||
colStart={1}
|
||||
{...borders}
|
||||
borderRightWidth={0}
|
||||
>
|
||||
<ScenariosHeader />
|
||||
</GridItem>
|
||||
|
||||
{scenarios.data.scenarios.map((scenario, i) => (
|
||||
<ScenarioRow
|
||||
rowStart={i + variantHeaderRows + scenarioHeaderRows + 2}
|
||||
key={scenario.uiId}
|
||||
scenario={scenario}
|
||||
variants={variants.data}
|
||||
canHide={visibleScenariosCount > 1}
|
||||
/>
|
||||
))}
|
||||
<GridItem
|
||||
rowStart={variantHeaderRows + scenarioHeaderRows + visibleScenariosCount + 2}
|
||||
colStart={1}
|
||||
colSpan={allCols}
|
||||
>
|
||||
<ScenarioPaginator />
|
||||
</GridItem>
|
||||
|
||||
{/* Add some extra padding on the right, because when the table is too wide to fit in the viewport `pr` on the Grid isn't respected. */}
|
||||
<GridItem rowStart={1} colStart={allCols} rowSpan={allRows} w={4} borderBottomWidth={0} />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
6
app/src/components/OutputsTable/styles.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type GridItemProps } from "@chakra-ui/react";
|
||||
|
||||
export const borders: GridItemProps = {
|
||||
borderRightWidth: 1,
|
||||
borderBottomWidth: 1,
|
||||
};
|
||||
@@ -2,4 +2,4 @@ import { type RouterOutputs } from "~/utils/api";
|
||||
|
||||
export type PromptVariant = NonNullable<RouterOutputs["promptVariants"]["list"]>[0];
|
||||
|
||||
export type Scenario = NonNullable<RouterOutputs["scenarios"]["list"]>[0];
|
||||
export type Scenario = NonNullable<RouterOutputs["scenarios"]["list"]>["scenarios"][0];
|
||||
59
app/src/components/RefinePromptModal/CompareFunctions.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { type StackProps, VStack, useBreakpointValue } from "@chakra-ui/react";
|
||||
import React from "react";
|
||||
import DiffViewer, { DiffMethod } from "react-diff-viewer";
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/components/prism-javascript";
|
||||
import "prismjs/themes/prism.css"; // choose a theme you like
|
||||
|
||||
const highlightSyntax = (str: string) => {
|
||||
let highlighted;
|
||||
try {
|
||||
highlighted = Prism.highlight(str, Prism.languages.javascript as Prism.Grammar, "javascript");
|
||||
} catch (e) {
|
||||
console.error("Error highlighting:", e);
|
||||
highlighted = str;
|
||||
}
|
||||
return <pre style={{ display: "inline" }} dangerouslySetInnerHTML={{ __html: highlighted }} />;
|
||||
};
|
||||
|
||||
const CompareFunctions = ({
|
||||
originalFunction,
|
||||
newFunction = "",
|
||||
leftTitle = "Original",
|
||||
rightTitle = "Modified",
|
||||
...props
|
||||
}: {
|
||||
originalFunction: string;
|
||||
newFunction?: string;
|
||||
leftTitle?: string;
|
||||
rightTitle?: string;
|
||||
} & StackProps) => {
|
||||
const showSplitView = useBreakpointValue(
|
||||
{
|
||||
base: false,
|
||||
md: true,
|
||||
},
|
||||
{
|
||||
fallback: "base",
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<VStack w="full" spacing={4} fontSize={12} lineHeight={1} overflowY="auto" {...props}>
|
||||
<DiffViewer
|
||||
oldValue={originalFunction}
|
||||
newValue={newFunction || originalFunction}
|
||||
splitView={showSplitView}
|
||||
hideLineNumbers={!showSplitView}
|
||||
leftTitle={leftTitle}
|
||||
rightTitle={rightTitle}
|
||||
disableWordDiff={true}
|
||||
compareMethod={DiffMethod.CHARS}
|
||||
renderContent={highlightSyntax}
|
||||
showDiffOnly={false}
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompareFunctions;
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Button, Spinner, InputGroup, InputRightElement, Icon, HStack } from "@chakra-ui/react";
|
||||
import { IoMdSend } from "react-icons/io";
|
||||
import AutoResizeTextArea from "../AutoResizeTextArea";
|
||||
|
||||
export const CustomInstructionsInput = ({
|
||||
instructions,
|
||||
setInstructions,
|
||||
loading,
|
||||
onSubmit,
|
||||
}: {
|
||||
instructions: string;
|
||||
setInstructions: (instructions: string) => void;
|
||||
loading: boolean;
|
||||
onSubmit: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<InputGroup
|
||||
size="md"
|
||||
w="full"
|
||||
maxW="600"
|
||||
boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);"
|
||||
borderRadius={8}
|
||||
alignItems="center"
|
||||
colorScheme="orange"
|
||||
>
|
||||
<AutoResizeTextArea
|
||||
value={instructions}
|
||||
onChange={(e) => setInstructions(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.metaKey && !e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
e.currentTarget.blur();
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
placeholder="Send custom instructions"
|
||||
py={4}
|
||||
pl={4}
|
||||
pr={12}
|
||||
colorScheme="orange"
|
||||
borderColor="gray.300"
|
||||
borderWidth={1}
|
||||
_hover={{
|
||||
borderColor: "gray.300",
|
||||
}}
|
||||
_focus={{
|
||||
borderColor: "gray.300",
|
||||
}}
|
||||
isDisabled={loading}
|
||||
/>
|
||||
<HStack></HStack>
|
||||
<InputRightElement width="8" height="full">
|
||||
<Button
|
||||
h="8"
|
||||
w="8"
|
||||
minW="unset"
|
||||
size="sm"
|
||||
onClick={() => onSubmit()}
|
||||
variant={instructions ? "solid" : "ghost"}
|
||||
mr={4}
|
||||
borderRadius="8"
|
||||
bgColor={instructions ? "orange.400" : "transparent"}
|
||||
colorScheme="orange"
|
||||
>
|
||||
{loading ? (
|
||||
<Spinner boxSize={4} />
|
||||
) : (
|
||||
<Icon as={IoMdSend} color={instructions ? "white" : "gray.500"} boxSize={5} />
|
||||
)}
|
||||
</Button>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
65
app/src/components/RefinePromptModal/RefineAction.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { HStack, Icon, Heading, Text, VStack, GridItem } from "@chakra-ui/react";
|
||||
import { type IconType } from "react-icons";
|
||||
import { BsStars } from "react-icons/bs";
|
||||
|
||||
export const RefineAction = ({
|
||||
label,
|
||||
icon,
|
||||
desciption,
|
||||
activeLabel,
|
||||
onClick,
|
||||
loading,
|
||||
}: {
|
||||
label: string;
|
||||
icon?: IconType;
|
||||
desciption: string;
|
||||
activeLabel: string | undefined;
|
||||
onClick: (label: string) => void;
|
||||
loading: boolean;
|
||||
}) => {
|
||||
const isActive = activeLabel === label;
|
||||
|
||||
return (
|
||||
<GridItem w="80" h="44">
|
||||
<VStack
|
||||
w="full"
|
||||
h="full"
|
||||
onClick={() => {
|
||||
!loading && onClick(label);
|
||||
}}
|
||||
borderColor={isActive ? "blue.500" : "gray.200"}
|
||||
borderWidth={2}
|
||||
borderRadius={16}
|
||||
padding={6}
|
||||
backgroundColor="gray.50"
|
||||
_hover={
|
||||
loading
|
||||
? undefined
|
||||
: {
|
||||
backgroundColor: "gray.100",
|
||||
}
|
||||
}
|
||||
spacing={8}
|
||||
boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);"
|
||||
cursor="pointer"
|
||||
opacity={loading ? 0.5 : 1}
|
||||
>
|
||||
<HStack cursor="pointer" spacing={6} fontSize="sm" fontWeight="medium" color="gray.500">
|
||||
<Icon as={icon || BsStars} boxSize={12} />
|
||||
<Heading size="md" fontFamily="inconsolata, monospace">
|
||||
{label}
|
||||
</Heading>
|
||||
</HStack>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color="gray.500"
|
||||
flexWrap="wrap"
|
||||
wordBreak="break-word"
|
||||
overflowWrap="break-word"
|
||||
>
|
||||
{desciption}
|
||||
</Text>
|
||||
</VStack>
|
||||
</GridItem>
|
||||
);
|
||||
};
|
||||
151
app/src/components/RefinePromptModal/RefinePromptModal.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
VStack,
|
||||
Text,
|
||||
Spinner,
|
||||
HStack,
|
||||
Icon,
|
||||
SimpleGrid,
|
||||
} from "@chakra-ui/react";
|
||||
import { BsStars } from "react-icons/bs";
|
||||
import { api } from "~/utils/api";
|
||||
import { useHandledAsyncCallback, useVisibleScenarioIds } from "~/utils/hooks";
|
||||
import { type PromptVariant } from "@prisma/client";
|
||||
import { useState } from "react";
|
||||
import CompareFunctions from "./CompareFunctions";
|
||||
import { CustomInstructionsInput } from "./CustomInstructionsInput";
|
||||
import { RefineAction } from "./RefineAction";
|
||||
import { isObject, isString } from "lodash-es";
|
||||
import { type RefinementAction, type SupportedProvider } from "~/modelProviders/types";
|
||||
import frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
||||
|
||||
export const RefinePromptModal = ({
|
||||
variant,
|
||||
onClose,
|
||||
}: {
|
||||
variant: PromptVariant;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const utils = api.useContext();
|
||||
const visibleScenarios = useVisibleScenarioIds();
|
||||
|
||||
const refinementActions =
|
||||
frontendModelProviders[variant.modelProvider as SupportedProvider].refinementActions || {};
|
||||
|
||||
const { mutateAsync: getModifiedPromptMutateAsync, data: refinedPromptFn } =
|
||||
api.promptVariants.getModifiedPromptFn.useMutation();
|
||||
const [instructions, setInstructions] = useState<string>("");
|
||||
|
||||
const [activeRefineActionLabel, setActiveRefineActionLabel] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const [getModifiedPromptFn, modificationInProgress] = useHandledAsyncCallback(
|
||||
async (label?: string) => {
|
||||
if (!variant.experimentId) return;
|
||||
const updatedInstructions = label
|
||||
? (refinementActions[label] as RefinementAction).instructions
|
||||
: instructions;
|
||||
setActiveRefineActionLabel(label);
|
||||
await getModifiedPromptMutateAsync({
|
||||
id: variant.id,
|
||||
instructions: updatedInstructions,
|
||||
});
|
||||
},
|
||||
[getModifiedPromptMutateAsync, onClose, variant, instructions, setActiveRefineActionLabel],
|
||||
);
|
||||
|
||||
const replaceVariantMutation = api.promptVariants.replaceVariant.useMutation();
|
||||
|
||||
const [replaceVariant, replacementInProgress] = useHandledAsyncCallback(async () => {
|
||||
if (
|
||||
!variant.experimentId ||
|
||||
!refinedPromptFn ||
|
||||
(isObject(refinedPromptFn) && "status" in refinedPromptFn)
|
||||
)
|
||||
return;
|
||||
await replaceVariantMutation.mutateAsync({
|
||||
id: variant.id,
|
||||
constructFn: refinedPromptFn,
|
||||
streamScenarios: visibleScenarios,
|
||||
});
|
||||
await utils.promptVariants.list.invalidate();
|
||||
onClose();
|
||||
}, [replaceVariantMutation, variant, onClose, refinedPromptFn]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
size={{ base: "xl", sm: "2xl", md: "3xl", lg: "5xl", xl: "7xl" }}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent w={1200}>
|
||||
<ModalHeader>
|
||||
<HStack>
|
||||
<Icon as={BsStars} />
|
||||
<Text>Refine with GPT-4</Text>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody maxW="unset">
|
||||
<VStack spacing={8}>
|
||||
<VStack spacing={4}>
|
||||
{Object.keys(refinementActions).length && (
|
||||
<>
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={8}>
|
||||
{Object.keys(refinementActions).map((label) => (
|
||||
<RefineAction
|
||||
key={label}
|
||||
label={label}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
icon={refinementActions[label]!.icon}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
desciption={refinementActions[label]!.description}
|
||||
activeLabel={activeRefineActionLabel}
|
||||
onClick={getModifiedPromptFn}
|
||||
loading={modificationInProgress}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<Text color="gray.500">or</Text>
|
||||
</>
|
||||
)}
|
||||
<CustomInstructionsInput
|
||||
instructions={instructions}
|
||||
setInstructions={setInstructions}
|
||||
loading={modificationInProgress}
|
||||
onSubmit={getModifiedPromptFn}
|
||||
/>
|
||||
</VStack>
|
||||
<CompareFunctions
|
||||
originalFunction={variant.constructFn}
|
||||
newFunction={isString(refinedPromptFn) ? refinedPromptFn : undefined}
|
||||
maxH="40vh"
|
||||
/>
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<HStack spacing={4}>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
onClick={replaceVariant}
|
||||
minW={24}
|
||||
isDisabled={replacementInProgress || !refinedPromptFn}
|
||||
>
|
||||
{replacementInProgress ? <Spinner boxSize={4} /> : <Text>Accept</Text>}
|
||||
</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
141
app/src/components/VariantHeader/VariantHeader.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { useState, type DragEvent } from "react";
|
||||
import { type PromptVariant } from "../OutputsTable/types";
|
||||
import { api } from "~/utils/api";
|
||||
import { RiDraggable } from "react-icons/ri";
|
||||
import { useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
||||
import { HStack, Icon, Text, GridItem, type GridItemProps } from "@chakra-ui/react"; // Changed here
|
||||
import { cellPadding, headerMinHeight } from "../constants";
|
||||
import AutoResizeTextArea from "../AutoResizeTextArea";
|
||||
import VariantHeaderMenuButton from "./VariantHeaderMenuButton";
|
||||
|
||||
export default function VariantHeader(
|
||||
allProps: {
|
||||
variant: PromptVariant;
|
||||
canHide: boolean;
|
||||
} & GridItemProps,
|
||||
) {
|
||||
const { variant, canHide, ...gridItemProps } = allProps;
|
||||
const { canModify } = useExperimentAccess();
|
||||
const utils = api.useContext();
|
||||
const [isDragTarget, setIsDragTarget] = useState(false);
|
||||
const [isInputHovered, setIsInputHovered] = useState(false);
|
||||
const [label, setLabel] = useState(variant.label);
|
||||
|
||||
const updateMutation = api.promptVariants.update.useMutation();
|
||||
const [onSaveLabel] = useHandledAsyncCallback(async () => {
|
||||
if (label && label !== variant.label) {
|
||||
await updateMutation.mutateAsync({
|
||||
id: variant.id,
|
||||
updates: { label: label },
|
||||
});
|
||||
}
|
||||
}, [updateMutation, variant.id, variant.label, label]);
|
||||
|
||||
const reorderMutation = api.promptVariants.reorder.useMutation();
|
||||
const [onReorder] = useHandledAsyncCallback(
|
||||
async (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragTarget(false);
|
||||
const draggedId = e.dataTransfer.getData("text/plain");
|
||||
const droppedId = variant.id;
|
||||
if (!draggedId || !droppedId || draggedId === droppedId) return;
|
||||
await reorderMutation.mutateAsync({
|
||||
draggedId,
|
||||
droppedId,
|
||||
});
|
||||
await utils.promptVariants.list.invalidate();
|
||||
},
|
||||
[reorderMutation, variant.id],
|
||||
);
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
if (!canModify) {
|
||||
return (
|
||||
<GridItem
|
||||
padding={0}
|
||||
sx={{
|
||||
position: "sticky",
|
||||
top: "0",
|
||||
// Ensure that the menu always appears above the sticky header of other variants
|
||||
zIndex: menuOpen ? "dropdown" : 10,
|
||||
}}
|
||||
borderTopWidth={1}
|
||||
{...gridItemProps}
|
||||
>
|
||||
<Text fontSize={16} fontWeight="bold" px={cellPadding.x} py={cellPadding.y}>
|
||||
{variant.label}
|
||||
</Text>
|
||||
</GridItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GridItem
|
||||
padding={0}
|
||||
sx={{
|
||||
position: "sticky",
|
||||
top: "0",
|
||||
// Ensure that the menu always appears above the sticky header of other variants
|
||||
zIndex: menuOpen ? "dropdown" : 10,
|
||||
}}
|
||||
borderTopWidth={1}
|
||||
{...gridItemProps}
|
||||
>
|
||||
<HStack
|
||||
spacing={2}
|
||||
alignItems="flex-start"
|
||||
minH={headerMinHeight}
|
||||
draggable={!isInputHovered}
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData("text/plain", variant.id);
|
||||
e.currentTarget.style.opacity = "0.4";
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
e.currentTarget.style.opacity = "1";
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDragTarget(true);
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
setIsDragTarget(false);
|
||||
}}
|
||||
onDrop={onReorder}
|
||||
backgroundColor={isDragTarget ? "gray.200" : "gray.100"}
|
||||
h="full"
|
||||
>
|
||||
<Icon
|
||||
as={RiDraggable}
|
||||
boxSize={6}
|
||||
mt={2}
|
||||
color="gray.400"
|
||||
_hover={{ color: "gray.800", cursor: "pointer" }}
|
||||
/>
|
||||
<AutoResizeTextArea
|
||||
size="sm"
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
onBlur={onSaveLabel}
|
||||
placeholder="Variant Name"
|
||||
borderWidth={1}
|
||||
borderColor="transparent"
|
||||
fontWeight="bold"
|
||||
fontSize={16}
|
||||
_hover={{ borderColor: "gray.300" }}
|
||||
_focus={{ borderColor: "blue.500", outline: "none" }}
|
||||
flex={1}
|
||||
px={cellPadding.x}
|
||||
onMouseEnter={() => setIsInputHovered(true)}
|
||||
onMouseLeave={() => setIsInputHovered(false)}
|
||||
/>
|
||||
<VariantHeaderMenuButton
|
||||
variant={variant}
|
||||
canHide={canHide}
|
||||
menuOpen={menuOpen}
|
||||
setMenuOpen={setMenuOpen}
|
||||
/>
|
||||
</HStack>
|
||||
</GridItem>
|
||||
);
|
||||
}
|
||||
107
app/src/components/VariantHeader/VariantHeaderMenuButton.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { type PromptVariant } from "../OutputsTable/types";
|
||||
import { api } from "~/utils/api";
|
||||
import { useHandledAsyncCallback, useVisibleScenarioIds } from "~/utils/hooks";
|
||||
import {
|
||||
Icon,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
MenuDivider,
|
||||
Text,
|
||||
Spinner,
|
||||
IconButton,
|
||||
} from "@chakra-ui/react";
|
||||
import { BsFillTrashFill, BsGear, BsStars } from "react-icons/bs";
|
||||
import { FaRegClone } from "react-icons/fa";
|
||||
import { useState } from "react";
|
||||
import { RefinePromptModal } from "../RefinePromptModal/RefinePromptModal";
|
||||
import { RiExchangeFundsFill } from "react-icons/ri";
|
||||
import { ChangeModelModal } from "../ChangeModelModal/ChangeModelModal";
|
||||
|
||||
export default function VariantHeaderMenuButton({
|
||||
variant,
|
||||
canHide,
|
||||
menuOpen,
|
||||
setMenuOpen,
|
||||
}: {
|
||||
variant: PromptVariant;
|
||||
canHide: boolean;
|
||||
menuOpen: boolean;
|
||||
setMenuOpen: (open: boolean) => void;
|
||||
}) {
|
||||
const utils = api.useContext();
|
||||
|
||||
const duplicateMutation = api.promptVariants.create.useMutation();
|
||||
const visibleScenarios = useVisibleScenarioIds();
|
||||
|
||||
const [duplicateVariant, duplicationInProgress] = useHandledAsyncCallback(async () => {
|
||||
await duplicateMutation.mutateAsync({
|
||||
experimentId: variant.experimentId,
|
||||
variantId: variant.id,
|
||||
streamScenarios: visibleScenarios,
|
||||
});
|
||||
await utils.promptVariants.list.invalidate();
|
||||
}, [duplicateMutation, variant.experimentId, variant.id]);
|
||||
|
||||
const hideMutation = api.promptVariants.hide.useMutation();
|
||||
const [onHide] = useHandledAsyncCallback(async () => {
|
||||
await hideMutation.mutateAsync({
|
||||
id: variant.id,
|
||||
});
|
||||
await utils.promptVariants.list.invalidate();
|
||||
}, [hideMutation, variant.id]);
|
||||
|
||||
const [changeModelModalOpen, setChangeModelModalOpen] = useState(false);
|
||||
const [refinePromptModalOpen, setRefinePromptModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu isOpen={menuOpen} onOpen={() => setMenuOpen(true)} onClose={() => setMenuOpen(false)}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
variant="ghost"
|
||||
aria-label="Edit Scenarios"
|
||||
icon={<Icon as={duplicationInProgress ? Spinner : BsGear} />}
|
||||
/>
|
||||
|
||||
<MenuList mt={-3} fontSize="md">
|
||||
<MenuItem icon={<Icon as={FaRegClone} boxSize={4} w={5} />} onClick={duplicateVariant}>
|
||||
Duplicate
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<Icon as={RiExchangeFundsFill} boxSize={5} />}
|
||||
onClick={() => setChangeModelModalOpen(true)}
|
||||
>
|
||||
Change Model
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<Icon as={BsStars} boxSize={5} />}
|
||||
onClick={() => setRefinePromptModalOpen(true)}
|
||||
>
|
||||
Refine
|
||||
</MenuItem>
|
||||
{canHide && (
|
||||
<>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
onClick={onHide}
|
||||
icon={<Icon as={BsFillTrashFill} boxSize={5} />}
|
||||
color="red.600"
|
||||
_hover={{ backgroundColor: "red.50" }}
|
||||
>
|
||||
<Text>Hide</Text>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
{changeModelModalOpen && (
|
||||
<ChangeModelModal variant={variant} onClose={() => setChangeModelModalOpen(false)} />
|
||||
)}
|
||||
{refinePromptModalOpen && (
|
||||
<RefinePromptModal variant={variant} onClose={() => setRefinePromptModalOpen(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
114
app/src/components/experiments/ExperimentCard.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
HStack,
|
||||
Icon,
|
||||
VStack,
|
||||
Text,
|
||||
Divider,
|
||||
Spinner,
|
||||
AspectRatio,
|
||||
SkeletonText,
|
||||
} from "@chakra-ui/react";
|
||||
import { RiFlaskLine } from "react-icons/ri";
|
||||
import { formatTimePast } from "~/utils/dayjs";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { BsPlusSquare } from "react-icons/bs";
|
||||
import { api } from "~/utils/api";
|
||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||
|
||||
type ExperimentData = {
|
||||
testScenarioCount: number;
|
||||
promptVariantCount: number;
|
||||
id: string;
|
||||
label: string;
|
||||
sortIndex: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => {
|
||||
return (
|
||||
<AspectRatio ratio={1.2} w="full">
|
||||
<VStack
|
||||
as={Link}
|
||||
href={{ pathname: "/experiments/[id]", query: { id: exp.id } }}
|
||||
bg="gray.50"
|
||||
_hover={{ bg: "gray.100" }}
|
||||
transition="background 0.2s"
|
||||
cursor="pointer"
|
||||
borderColor="gray.200"
|
||||
borderWidth={1}
|
||||
p={4}
|
||||
justify="space-between"
|
||||
>
|
||||
<HStack w="full" color="gray.700" justify="center">
|
||||
<Icon as={RiFlaskLine} boxSize={4} />
|
||||
<Text fontWeight="bold">{exp.label}</Text>
|
||||
</HStack>
|
||||
<HStack h="full" spacing={4} flex={1} align="center">
|
||||
<CountLabel label="Variants" count={exp.promptVariantCount} />
|
||||
<Divider h={12} orientation="vertical" />
|
||||
<CountLabel label="Scenarios" count={exp.testScenarioCount} />
|
||||
</HStack>
|
||||
<HStack w="full" color="gray.500" fontSize="xs" textAlign="center">
|
||||
<Text flex={1}>Created {formatTimePast(exp.createdAt)}</Text>
|
||||
<Divider h={4} orientation="vertical" />
|
||||
<Text flex={1}>Updated {formatTimePast(exp.updatedAt)}</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</AspectRatio>
|
||||
);
|
||||
};
|
||||
|
||||
const CountLabel = ({ label, count }: { label: string; count: number }) => {
|
||||
return (
|
||||
<VStack alignItems="center" flex={1}>
|
||||
<Text color="gray.500" fontWeight="bold">
|
||||
{label}
|
||||
</Text>
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
{count}
|
||||
</Text>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewExperimentCard = () => {
|
||||
const router = useRouter();
|
||||
const createMutation = api.experiments.create.useMutation();
|
||||
const [createExperiment, isLoading] = useHandledAsyncCallback(async () => {
|
||||
const newExperiment = await createMutation.mutateAsync({ label: "New Experiment" });
|
||||
await router.push({ pathname: "/experiments/[id]", query: { id: newExperiment.id } });
|
||||
}, [createMutation, router]);
|
||||
|
||||
return (
|
||||
<AspectRatio ratio={1.2} w="full">
|
||||
<VStack
|
||||
align="center"
|
||||
justify="center"
|
||||
_hover={{ cursor: "pointer", bg: "gray.50" }}
|
||||
transition="background 0.2s"
|
||||
cursor="pointer"
|
||||
borderColor="gray.200"
|
||||
borderWidth={1}
|
||||
p={4}
|
||||
onClick={createExperiment}
|
||||
>
|
||||
<Icon as={isLoading ? Spinner : BsPlusSquare} boxSize={8} />
|
||||
<Text display={{ base: "none", md: "block" }} ml={2}>
|
||||
New Experiment
|
||||
</Text>
|
||||
</VStack>
|
||||
</AspectRatio>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExperimentCardSkeleton = () => (
|
||||
<AspectRatio ratio={1.2} w="full">
|
||||
<VStack align="center" borderColor="gray.200" borderWidth={1} p={4} bg="gray.50">
|
||||
<SkeletonText noOfLines={1} w="80%" />
|
||||
<SkeletonText noOfLines={2} w="60%" />
|
||||
<SkeletonText noOfLines={1} w="80%" />
|
||||
</VStack>
|
||||
</AspectRatio>
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Button,
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogContent,
|
||||
AlertDialogOverlay,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import { useRef } from "react";
|
||||
import { api } from "~/utils/api";
|
||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||
|
||||
export const DeleteDialog = ({ onClose }: { onClose: () => void }) => {
|
||||
const experiment = useExperiment();
|
||||
const deleteMutation = api.experiments.delete.useMutation();
|
||||
const utils = api.useContext();
|
||||
const router = useRouter();
|
||||
|
||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const [onDeleteConfirm] = useHandledAsyncCallback(async () => {
|
||||
if (!experiment.data?.id) return;
|
||||
await deleteMutation.mutateAsync({ id: experiment.data.id });
|
||||
await utils.experiments.list.invalidate();
|
||||
await router.push({ pathname: "/experiments" });
|
||||
onClose();
|
||||
}, [deleteMutation, experiment.data?.id, router]);
|
||||
|
||||
return (
|
||||
<AlertDialog isOpen leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
Delete Experiment
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
If you delete this experiment all the associated prompts and scenarios will be deleted
|
||||
as well. Are you sure?
|
||||
</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="red" onClick={onDeleteConfirm} ml={3}>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Button, HStack, Icon, Spinner, Text } from "@chakra-ui/react";
|
||||
import { useOnForkButtonPressed } from "./useOnForkButtonPressed";
|
||||
import { useExperiment } from "~/utils/hooks";
|
||||
import { BsGearFill } from "react-icons/bs";
|
||||
import { TbGitFork } from "react-icons/tb";
|
||||
import { useAppStore } from "~/state/store";
|
||||
|
||||
export const HeaderButtons = () => {
|
||||
const experiment = useExperiment();
|
||||
|
||||
const canModify = experiment.data?.access.canModify ?? false;
|
||||
|
||||
const { onForkButtonPressed, isForking } = useOnForkButtonPressed();
|
||||
|
||||
const openDrawer = useAppStore((s) => s.openDrawer);
|
||||
|
||||
if (experiment.isLoading) return null;
|
||||
|
||||
return (
|
||||
<HStack spacing={0} mt={{ base: 2, md: 0 }}>
|
||||
<Button
|
||||
onClick={onForkButtonPressed}
|
||||
mr={4}
|
||||
colorScheme={canModify ? undefined : "orange"}
|
||||
bgColor={canModify ? undefined : "orange.400"}
|
||||
minW={0}
|
||||
variant={{ base: "solid", md: canModify ? "ghost" : "solid" }}
|
||||
>
|
||||
{isForking ? <Spinner boxSize={5} /> : <Icon as={TbGitFork} boxSize={5} />}
|
||||
<Text ml={2}>Fork</Text>
|
||||
</Button>
|
||||
{canModify && (
|
||||
<Button variant={{ base: "solid", md: "ghost" }} onClick={openDrawer}>
|
||||
<HStack>
|
||||
<Icon as={BsGearFill} />
|
||||
<Text>Settings</Text>
|
||||
</HStack>
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useCallback } from "react";
|
||||
import { api } from "~/utils/api";
|
||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const useOnForkButtonPressed = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const user = useSession().data;
|
||||
const experiment = useExperiment();
|
||||
|
||||
const forkMutation = api.experiments.fork.useMutation();
|
||||
|
||||
const [onFork, isForking] = useHandledAsyncCallback(async () => {
|
||||
if (!experiment.data?.id) return;
|
||||
const forkedExperimentId = await forkMutation.mutateAsync({ id: experiment.data.id });
|
||||
await router.push({ pathname: "/experiments/[id]", query: { id: forkedExperimentId } });
|
||||
}, [forkMutation, experiment.data?.id, router]);
|
||||
|
||||
const onForkButtonPressed = useCallback(() => {
|
||||
if (user === null) {
|
||||
signIn("github").catch(console.error);
|
||||
} else {
|
||||
onFork();
|
||||
}
|
||||
}, [onFork, user]);
|
||||
|
||||
return { onForkButtonPressed, isForking };
|
||||
};
|
||||
134
app/src/components/nav/AppShell.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Heading,
|
||||
VStack,
|
||||
Icon,
|
||||
HStack,
|
||||
Image,
|
||||
Text,
|
||||
Box,
|
||||
type BoxProps,
|
||||
type LinkProps,
|
||||
Link,
|
||||
Flex,
|
||||
} from "@chakra-ui/react";
|
||||
import Head from "next/head";
|
||||
import { BsGithub, BsPersonCircle } from "react-icons/bs";
|
||||
import { useRouter } from "next/router";
|
||||
import { type IconType } from "react-icons";
|
||||
import { RiFlaskLine } from "react-icons/ri";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import UserMenu from "./UserMenu";
|
||||
|
||||
type IconLinkProps = BoxProps & LinkProps & { label?: string; icon: IconType };
|
||||
|
||||
const IconLink = ({ icon, label, href, target, color, ...props }: IconLinkProps) => {
|
||||
const router = useRouter();
|
||||
const isActive = href && router.pathname.startsWith(href);
|
||||
return (
|
||||
<HStack
|
||||
w="full"
|
||||
p={4}
|
||||
color={color}
|
||||
as={Link}
|
||||
href={href}
|
||||
target={target}
|
||||
bgColor={isActive ? "gray.200" : "transparent"}
|
||||
_hover={{ bgColor: "gray.200", textDecoration: "none" }}
|
||||
justifyContent="start"
|
||||
cursor="pointer"
|
||||
{...props}
|
||||
>
|
||||
<Icon as={icon} boxSize={6} mr={2} />
|
||||
<Text fontWeight="bold" fontSize="sm">
|
||||
{label}
|
||||
</Text>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
const Divider = () => <Box h="1px" bgColor="gray.200" />;
|
||||
|
||||
const NavSidebar = () => {
|
||||
const user = useSession().data;
|
||||
|
||||
return (
|
||||
<VStack
|
||||
align="stretch"
|
||||
bgColor="gray.100"
|
||||
py={2}
|
||||
pb={0}
|
||||
height="100%"
|
||||
w={{ base: "56px", md: "200px" }}
|
||||
overflow="hidden"
|
||||
>
|
||||
<HStack as={Link} href="/" _hover={{ textDecoration: "none" }} spacing={0} px={4} py={2}>
|
||||
<Image src="/logo.svg" alt="" boxSize={6} mr={4} />
|
||||
<Heading size="md" fontFamily="inconsolata, monospace">
|
||||
OpenPipe
|
||||
</Heading>
|
||||
</HStack>
|
||||
<VStack spacing={0} align="flex-start" overflowY="auto" overflowX="hidden" flex={1}>
|
||||
{user != null && (
|
||||
<>
|
||||
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
||||
</>
|
||||
)}
|
||||
{user === null && (
|
||||
<IconLink
|
||||
icon={BsPersonCircle}
|
||||
label="Sign In"
|
||||
onClick={() => {
|
||||
signIn("github").catch(console.error);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
{user ? <UserMenu user={user} /> : <Divider />}
|
||||
<VStack spacing={0} align="center">
|
||||
<Link
|
||||
href="https://github.com/openpipe/openpipe"
|
||||
target="_blank"
|
||||
color="gray.500"
|
||||
_hover={{ color: "gray.800" }}
|
||||
p={2}
|
||||
>
|
||||
<Icon as={BsGithub} boxSize={6} />
|
||||
</Link>
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default function AppShell(props: { children: React.ReactNode; title?: string }) {
|
||||
const [vh, setVh] = useState("100vh"); // Default height to prevent flicker on initial render
|
||||
|
||||
useEffect(() => {
|
||||
const setHeight = () => {
|
||||
const vh = window.innerHeight * 0.01;
|
||||
document.documentElement.style.setProperty("--vh", `${vh}px`);
|
||||
setVh(`calc(var(--vh, 1vh) * 100)`);
|
||||
};
|
||||
setHeight(); // Set the height at the start
|
||||
|
||||
window.addEventListener("resize", setHeight);
|
||||
window.addEventListener("orientationchange", setHeight);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", setHeight);
|
||||
window.removeEventListener("orientationchange", setHeight);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex h={vh} w="100vw">
|
||||
<Head>
|
||||
<title>{props.title ? `${props.title} | OpenPipe` : "OpenPipe"}</title>
|
||||
</Head>
|
||||
<NavSidebar />
|
||||
<Box h="100%" flex={1} overflowY="auto">
|
||||
{props.children}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
74
app/src/components/nav/UserMenu.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
HStack,
|
||||
Icon,
|
||||
Image,
|
||||
VStack,
|
||||
Text,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
Link,
|
||||
} from "@chakra-ui/react";
|
||||
import { type Session } from "next-auth";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { BsBoxArrowRight, BsChevronRight, BsPersonCircle } from "react-icons/bs";
|
||||
|
||||
export default function UserMenu({ user }: { user: Session }) {
|
||||
const profileImage = user.user.image ? (
|
||||
<Image src={user.user.image} alt="profile picture" boxSize={8} borderRadius="50%" />
|
||||
) : (
|
||||
<Icon as={BsPersonCircle} boxSize={6} />
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover placement="right">
|
||||
<PopoverTrigger>
|
||||
<HStack
|
||||
// Weird values to make mobile look right; can clean up when we make the sidebar disappear on mobile
|
||||
px={3}
|
||||
spacing={3}
|
||||
py={2}
|
||||
borderColor={"gray.200"}
|
||||
borderTopWidth={1}
|
||||
borderBottomWidth={1}
|
||||
cursor="pointer"
|
||||
_hover={{
|
||||
bgColor: "gray.200",
|
||||
}}
|
||||
>
|
||||
{profileImage}
|
||||
<VStack spacing={0} align="start" flex={1} flexShrink={1}>
|
||||
<Text fontWeight="bold" fontSize="sm">
|
||||
{user.user.name}
|
||||
</Text>
|
||||
<Text color="gray.500" fontSize="xs">
|
||||
{user.user.email}
|
||||
</Text>
|
||||
</VStack>
|
||||
<Icon as={BsChevronRight} boxSize={4} color="gray.500" />
|
||||
</HStack>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent _focusVisible={{ boxShadow: "unset", outline: "unset" }} maxW="200px">
|
||||
<VStack align="stretch" spacing={0}>
|
||||
{/* sign out */}
|
||||
<HStack
|
||||
as={Link}
|
||||
onClick={() => {
|
||||
signOut().catch(console.error);
|
||||
}}
|
||||
px={4}
|
||||
py={2}
|
||||
spacing={4}
|
||||
color="gray.500"
|
||||
fontSize="sm"
|
||||
>
|
||||
<Icon as={BsBoxArrowRight} boxSize={6} />
|
||||
<Text>Sign out</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,6 @@ export const CostTooltip = ({
|
||||
color="gray.800"
|
||||
bgColor="gray.50"
|
||||
borderWidth={1}
|
||||
py={2}
|
||||
hasArrow
|
||||
shouldWrapChildren
|
||||
label={
|
||||
@@ -9,7 +9,16 @@ export const env = createEnv({
|
||||
server: {
|
||||
DATABASE_URL: z.string().url(),
|
||||
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
||||
RESTRICT_PRISMA_LOGS: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("false")
|
||||
.transform((val) => val.toLowerCase() === "true"),
|
||||
GITHUB_CLIENT_ID: z.string().min(1),
|
||||
GITHUB_CLIENT_SECRET: z.string().min(1),
|
||||
OPENAI_API_KEY: z.string().min(1),
|
||||
REPLICATE_API_TOKEN: z.string().default("placeholder"),
|
||||
ANTHROPIC_API_KEY: z.string().default("placeholder"),
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -19,11 +28,6 @@ export const env = createEnv({
|
||||
*/
|
||||
client: {
|
||||
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
||||
NEXT_PUBLIC_IS_PUBLIC_PLAYGROUND: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("false")
|
||||
.transform((val) => val.toLowerCase() === "true"),
|
||||
NEXT_PUBLIC_SOCKET_URL: z.string().url().default("http://localhost:3318"),
|
||||
},
|
||||
|
||||
@@ -35,9 +39,13 @@ export const env = createEnv({
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
RESTRICT_PRISMA_LOGS: process.env.RESTRICT_PRISMA_LOGS,
|
||||
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
||||
NEXT_PUBLIC_IS_PUBLIC_PLAYGROUND: process.env.NEXT_PUBLIC_IS_PUBLIC_PLAYGROUND,
|
||||
NEXT_PUBLIC_SOCKET_URL: process.env.NEXT_PUBLIC_SOCKET_URL,
|
||||
GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
|
||||
GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
|
||||
REPLICATE_API_TOKEN: process.env.REPLICATE_API_TOKEN,
|
||||
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
||||
},
|
||||
/**
|
||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
|
||||
69
app/src/modelProviders/anthropic/codegen/codegen.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
import YAML from "yaml";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { openapiSchemaToJsonSchema } from "@openapi-contrib/openapi-schema-to-json-schema";
|
||||
import $RefParser from "@apidevtools/json-schema-ref-parser";
|
||||
import { type JSONObject } from "superjson/dist/types";
|
||||
import assert from "assert";
|
||||
import { type JSONSchema4Object } from "json-schema";
|
||||
import { isObject } from "lodash-es";
|
||||
|
||||
// @ts-expect-error for some reason missing from types
|
||||
import parserEstree from "prettier/plugins/estree";
|
||||
import parserBabel from "prettier/plugins/babel";
|
||||
import prettier from "prettier/standalone";
|
||||
|
||||
const OPENAPI_URL =
|
||||
"https://raw.githubusercontent.com/tryAGI/Anthropic/1c0871e861de60a4c3a843cb90e17d63e86c234a/docs/openapi.yaml";
|
||||
|
||||
// Fetch the openapi document
|
||||
const response = await fetch(OPENAPI_URL);
|
||||
const openApiYaml = await response.text();
|
||||
|
||||
// Parse the yaml document
|
||||
let schema = YAML.parse(openApiYaml) as JSONObject;
|
||||
schema = openapiSchemaToJsonSchema(schema);
|
||||
|
||||
const jsonSchema = await $RefParser.dereference(schema);
|
||||
|
||||
assert("components" in jsonSchema);
|
||||
const completionRequestSchema = jsonSchema.components.schemas
|
||||
.CreateCompletionRequest as JSONSchema4Object;
|
||||
|
||||
// We need to do a bit of surgery here since the Monaco editor doesn't like
|
||||
// the fact that the schema says `model` can be either a string or an enum,
|
||||
// and displays a warning in the editor. Let's stick with just an enum for
|
||||
// now and drop the string option.
|
||||
assert(
|
||||
"properties" in completionRequestSchema &&
|
||||
isObject(completionRequestSchema.properties) &&
|
||||
"model" in completionRequestSchema.properties &&
|
||||
isObject(completionRequestSchema.properties.model),
|
||||
);
|
||||
|
||||
const modelProperty = completionRequestSchema.properties.model;
|
||||
assert(
|
||||
"oneOf" in modelProperty &&
|
||||
Array.isArray(modelProperty.oneOf) &&
|
||||
modelProperty.oneOf.length === 2 &&
|
||||
isObject(modelProperty.oneOf[1]) &&
|
||||
"enum" in modelProperty.oneOf[1],
|
||||
"Expected model to have oneOf length of 2",
|
||||
);
|
||||
modelProperty.type = "string";
|
||||
modelProperty.enum = modelProperty.oneOf[1].enum;
|
||||
delete modelProperty["oneOf"];
|
||||
|
||||
// Get the directory of the current script
|
||||
const currentDirectory = path.dirname(import.meta.url).replace("file://", "");
|
||||
|
||||
// Write the JSON schema to a file in the current directory
|
||||
fs.writeFileSync(
|
||||
path.join(currentDirectory, "input.schema.json"),
|
||||
await prettier.format(JSON.stringify(completionRequestSchema, null, 2), {
|
||||
parser: "json",
|
||||
plugins: [parserBabel, parserEstree],
|
||||
}),
|
||||
);
|
||||
129
app/src/modelProviders/anthropic/codegen/input.schema.json
Normal file
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"description": "The model that will complete your prompt.\nAs we improve Claude, we develop new versions of it that you can query.\nThis parameter controls which version of Claude answers your request.\nRight now we are offering two model families: Claude, and Claude Instant.\nYou can use them by setting model to \"claude-2\" or \"claude-instant-1\", respectively.\nSee models for additional details.\n",
|
||||
"x-oaiTypeLabel": "string",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"claude-2",
|
||||
"claude-2.0",
|
||||
"claude-instant-1",
|
||||
"claude-instant-1.1"
|
||||
]
|
||||
},
|
||||
"prompt": {
|
||||
"description": "The prompt that you want Claude to complete.\n\nFor proper response generation you will need to format your prompt as follows:\n\\n\\nHuman: ${userQuestion}\\n\\nAssistant:\nSee our comments on prompts for more context.\n",
|
||||
"default": "<|endoftext|>",
|
||||
"nullable": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"example": "This is a test."
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"example": "This is a test."
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"example": "[1212, 318, 257, 1332, 13]"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"example": "[[1212, 318, 257, 1332, 13]]"
|
||||
}
|
||||
]
|
||||
},
|
||||
"max_tokens_to_sample": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"default": 256,
|
||||
"example": 256,
|
||||
"nullable": true,
|
||||
"description": "The maximum number of tokens to generate before stopping.\n\nNote that our models may stop before reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate.\n"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"default": 1,
|
||||
"example": 1,
|
||||
"nullable": true,
|
||||
"description": "Amount of randomness injected into the response.\n\nDefaults to 1. Ranges from 0 to 1. Use temp closer to 0 for analytical / multiple choice, and closer to 1 for creative and generative tasks.\n"
|
||||
},
|
||||
"top_p": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"default": 1,
|
||||
"example": 1,
|
||||
"nullable": true,
|
||||
"description": "Use nucleus sampling.\n\nIn nucleus sampling, we compute the cumulative distribution over all the options \nfor each subsequent token in decreasing probability order and cut it off once \nit reaches a particular probability specified by top_p. You should either alter temperature or top_p, but not both.\n"
|
||||
},
|
||||
"top_k": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"default": 5,
|
||||
"example": 5,
|
||||
"nullable": true,
|
||||
"description": "Only sample from the top K options for each subsequent token.\n\nUsed to remove \"long tail\" low probability responses. Learn more technical details here.\n"
|
||||
},
|
||||
"stream": {
|
||||
"description": "Whether to incrementally stream the response using server-sent events.\nSee this guide to SSE events for details.type: boolean\n",
|
||||
"nullable": true,
|
||||
"default": false
|
||||
},
|
||||
"stop_sequences": {
|
||||
"description": "Sequences that will cause the model to stop generating completion text.\nOur models stop on \"\\n\\nHuman:\", and may include additional built-in stop sequences in the future. By providing the stop_sequences parameter, you may include additional strings that will cause the model to stop generating.\n",
|
||||
"default": null,
|
||||
"nullable": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"default": "<|endoftext|>",
|
||||
"example": "\n",
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 4,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"example": "[\"\\n\"]"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"example": "13803d75-b4b5-4c3e-b2a2-6f21399b021b",
|
||||
"description": "An external identifier for the user who is associated with the request.\n\nThis should be a uuid, hash value, or other opaque identifier. Anthropic may use this id to help detect abuse. \nDo not include any identifying information such as name, email address, or phone number.\n"
|
||||
}
|
||||
},
|
||||
"description": "An object describing metadata about the request.\n"
|
||||
}
|
||||
},
|
||||
"required": ["model", "prompt", "max_tokens_to_sample"]
|
||||
}
|
||||
40
app/src/modelProviders/anthropic/frontend.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { type Completion } from "@anthropic-ai/sdk/resources";
|
||||
import { type SupportedModel } from ".";
|
||||
import { type FrontendModelProvider } from "../types";
|
||||
import { refinementActions } from "./refinementActions";
|
||||
|
||||
const frontendModelProvider: FrontendModelProvider<SupportedModel, Completion> = {
|
||||
name: "Replicate Llama2",
|
||||
|
||||
models: {
|
||||
"claude-2.0": {
|
||||
name: "Claude 2.0",
|
||||
contextWindow: 100000,
|
||||
promptTokenPrice: 11.02 / 1000000,
|
||||
completionTokenPrice: 32.68 / 1000000,
|
||||
speed: "medium",
|
||||
provider: "anthropic",
|
||||
learnMoreUrl: "https://www.anthropic.com/product",
|
||||
},
|
||||
"claude-instant-1.1": {
|
||||
name: "Claude Instant 1.1",
|
||||
contextWindow: 100000,
|
||||
promptTokenPrice: 1.63 / 1000000,
|
||||
completionTokenPrice: 5.51 / 1000000,
|
||||
speed: "fast",
|
||||
provider: "anthropic",
|
||||
learnMoreUrl: "https://www.anthropic.com/product",
|
||||
},
|
||||
},
|
||||
|
||||
refinementActions,
|
||||
|
||||
normalizeOutput: (output) => {
|
||||
return {
|
||||
type: "text",
|
||||
value: output.completion,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default frontendModelProvider;
|
||||
86
app/src/modelProviders/anthropic/getCompletion.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { env } from "~/env.mjs";
|
||||
import { type CompletionResponse } from "../types";
|
||||
|
||||
import Anthropic, { APIError } from "@anthropic-ai/sdk";
|
||||
import { type Completion, type CompletionCreateParams } from "@anthropic-ai/sdk/resources";
|
||||
import { isObject, isString } from "lodash-es";
|
||||
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: env.ANTHROPIC_API_KEY,
|
||||
});
|
||||
|
||||
export async function getCompletion(
|
||||
input: CompletionCreateParams,
|
||||
onStream: ((partialOutput: Completion) => void) | null,
|
||||
): Promise<CompletionResponse<Completion>> {
|
||||
const start = Date.now();
|
||||
let finalCompletion: Completion | null = null;
|
||||
|
||||
try {
|
||||
if (onStream) {
|
||||
const resp = await anthropic.completions.create(
|
||||
{ ...input, stream: true },
|
||||
{
|
||||
maxRetries: 0,
|
||||
},
|
||||
);
|
||||
|
||||
for await (const part of resp) {
|
||||
if (finalCompletion === null) {
|
||||
finalCompletion = part;
|
||||
} else {
|
||||
finalCompletion = { ...part, completion: finalCompletion.completion + part.completion };
|
||||
}
|
||||
onStream(finalCompletion);
|
||||
}
|
||||
if (!finalCompletion) {
|
||||
return {
|
||||
type: "error",
|
||||
message: "Streaming failed to return a completion",
|
||||
autoRetry: false,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const resp = await anthropic.completions.create(
|
||||
{ ...input, stream: false },
|
||||
{
|
||||
maxRetries: 0,
|
||||
},
|
||||
);
|
||||
finalCompletion = resp;
|
||||
}
|
||||
const timeToComplete = Date.now() - start;
|
||||
|
||||
return {
|
||||
type: "success",
|
||||
statusCode: 200,
|
||||
value: finalCompletion,
|
||||
timeToComplete,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
console.log("CAUGHT ERROR", error);
|
||||
if (error instanceof APIError) {
|
||||
const message =
|
||||
isObject(error.error) &&
|
||||
"error" in error.error &&
|
||||
isObject(error.error.error) &&
|
||||
"message" in error.error.error &&
|
||||
isString(error.error.error.message)
|
||||
? error.error.error.message
|
||||
: error.message;
|
||||
|
||||
return {
|
||||
type: "error",
|
||||
message,
|
||||
autoRetry: error.status === 429 || error.status === 503,
|
||||
statusCode: error.status,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "error",
|
||||
message: (error as Error).message,
|
||||
autoRetry: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
34
app/src/modelProviders/anthropic/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { type JSONSchema4 } from "json-schema";
|
||||
import { type ModelProvider } from "../types";
|
||||
import inputSchema from "./codegen/input.schema.json";
|
||||
import { getCompletion } from "./getCompletion";
|
||||
import frontendModelProvider from "./frontend";
|
||||
import type { Completion, CompletionCreateParams } from "@anthropic-ai/sdk/resources";
|
||||
|
||||
const supportedModels = ["claude-2.0", "claude-instant-1.1"] as const;
|
||||
|
||||
export type SupportedModel = (typeof supportedModels)[number];
|
||||
|
||||
export type AnthropicProvider = ModelProvider<SupportedModel, CompletionCreateParams, Completion>;
|
||||
|
||||
const modelProvider: AnthropicProvider = {
|
||||
getModel: (input) => {
|
||||
if (supportedModels.includes(input.model as SupportedModel))
|
||||
return input.model as SupportedModel;
|
||||
|
||||
const modelMaps: Record<string, SupportedModel> = {
|
||||
"claude-2": "claude-2.0",
|
||||
"claude-instant-1": "claude-instant-1.1",
|
||||
};
|
||||
|
||||
if (input.model in modelMaps) return modelMaps[input.model] as SupportedModel;
|
||||
|
||||
return null;
|
||||
},
|
||||
inputSchema: inputSchema as JSONSchema4,
|
||||
canStream: true,
|
||||
getCompletion,
|
||||
...frontendModelProvider,
|
||||
};
|
||||
|
||||
export default modelProvider;
|
||||
3
app/src/modelProviders/anthropic/refinementActions.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { type RefinementAction } from "../types";
|
||||
|
||||
export const refinementActions: Record<string, RefinementAction> = {};
|
||||