Compare commits

..

1 Commits

Author SHA1 Message Date
David Corbitt
328cd4f5e6 Pass and log a function 2023-07-29 15:07:29 -07:00
329 changed files with 3573 additions and 13834 deletions

View File

@@ -1,5 +0,0 @@
**/node_modules/
.git
**/.venv/
**/.env*
**/.next/

View File

@@ -26,11 +26,6 @@ NEXT_PUBLIC_SOCKET_URL="http://localhost:3318"
NEXTAUTH_SECRET="your_secret"
NEXTAUTH_URL="http://localhost:3000"
NEXT_PUBLIC_HOST="http://localhost:3000"
# Next Auth Github Provider
GITHUB_CLIENT_ID="your_client_id"
GITHUB_CLIENT_SECRET="your_secret"
OPENPIPE_BASE_URL="http://localhost:3000/api"
OPENPIPE_API_KEY="your_key"

View File

@@ -6,7 +6,7 @@ const config = {
overrides: [
{
extends: ["plugin:@typescript-eslint/recommended-requiring-type-checking"],
files: ["*.mts", "*.ts", "*.tsx"],
files: ["*.ts", "*.tsx"],
parserOptions: {
project: path.join(__dirname, "tsconfig.json"),
},
@@ -37,7 +37,6 @@ const config = {
"warn",
{ vars: "all", varsIgnorePattern: "^_", args: "after-used", argsIgnorePattern: "^_" },
],
"react/no-unescaped-entities": "off",
},
};

View File

@@ -6,10 +6,6 @@ on:
push:
branches: [main]
defaults:
run:
working-directory: app
jobs:
run-checks:
runs-on: ubuntu-latest

43
.gitignore vendored
View File

@@ -1,5 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
# next.js
/.next/
/out/
next-env.d.ts
# production
/build
# misc
.DS_Store
*.pem
# debug
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
.venv/
*.pyc
node_modules/
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo

View File

@@ -12,23 +12,11 @@ declare module "nextjs-routes" {
export type Route =
| StaticRoute<"/account/signin">
| DynamicRoute<"/api/[...trpc]", { "trpc": string[] }>
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
| StaticRoute<"/api/experiments/og-image">
| StaticRoute<"/api/openapi">
| StaticRoute<"/api/sentry-example-api">
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
| StaticRoute<"/dashboard">
| DynamicRoute<"/data/[id]", { "id": string }>
| StaticRoute<"/data">
| DynamicRoute<"/experiments/[id]", { "id": string }>
| StaticRoute<"/experiments">
| StaticRoute<"/">
| StaticRoute<"/project/settings">
| StaticRoute<"/request-logs">
| StaticRoute<"/sentry-example-page">
| StaticRoute<"/world-champs">
| StaticRoute<"/world-champs/signup">;
| StaticRoute<"/">;
interface StaticRoute<Pathname> {
pathname: Pathname;

42
Dockerfile Normal file
View File

@@ -0,0 +1,42 @@
# Adapted from https://create.t3.gg/en/deployment/docker#3-create-dockerfile
FROM node:20.1.0-bullseye as base
RUN yarn global add pnpm
# DEPS
FROM base as deps
WORKDIR /app
COPY prisma ./
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# BUILDER
FROM base as builder
# Include all NEXT_PUBLIC_* env vars here
ARG NEXT_PUBLIC_POSTHOG_KEY
ARG NEXT_PUBLIC_SOCKET_URL
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN SKIP_ENV_VALIDATION=1 pnpm build
# RUNNER
FROM base as runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
COPY --from=builder /app/ ./
EXPOSE 3000
ENV PORT 3000
# Run the "run-prod.sh" script
CMD /app/run-prod.sh

View File

@@ -1,62 +1,52 @@
<!-- <img src="https://github.com/openpipe/openpipe/assets/41524992/ca59596e-eb80-40f9-921f-6d67f6e6d8fa" width="72px" /> -->
<img src="https://github.com/openpipe/openpipe/assets/41524992/ca59596e-eb80-40f9-921f-6d67f6e6d8fa" width="72px" />
# OpenPipe
OpenPipe is a flexible playground for comparing and optimizing LLM prompts. It lets you quickly generate, test and compare candidate prompts, and can automatically [translate](#-translate-between-model-apis) those prompts between models.
<img src="https://github.com/openpipe/openpipe/assets/41524992/219a844e-3f4e-4f6b-8066-41348b42977b" alt="demo">
You can use our hosted version of OpenPipe at https://openpipe.ai. You can also clone this repository and [run it locally](#running-locally).
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.
## Sample Experiments
These are simple experiments users have created that show how OpenPipe works. Feel free to fork them and start experimenting yourself.
These are simple experiments users have created that show how OpenPipe works.
- [Twitter Sentiment Analysis](https://app.openpipe.ai/experiments/62c20a73-2012-4a64-973c-4b665ad46a57)
- [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">
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
**Configure Multiple Prompts**
Set up multiple prompt configurations and compare their output side-by-side. Each configuration can be configured independently.
**Visualize Responses**
Inspect prompt completions side-by-side.
**Test Many Inputs**
OpenPipe lets you _template_ a prompt. Use the templating feature to run the prompts you're testing against many potential inputs for broader coverage of your problem space than you'd get with manual testing.
**🪄 Auto-generate Test Scenarios**
OpenPipe includes a tool to generate new test scenarios based on your existing prompts and scenarios. Just click "Autogenerate Scenario" to try it out!
**Prompt Validation and Typeahead**
We use OpenAI's OpenAPI spec to automatically provide typeahead and validate prompts.
<img alt="typeahead" src="https://github.com/openpipe/openpipe/assets/176426/acc638f8-d851-4742-8d01-fe6f98890840" height="300px">
**Function Call Support**
Natively supports [OpenAI function calls](https://openai.com/blog/function-calling-and-other-api-updates) on supported models.
<img height="300px" alt="function calls" src="https://github.com/openpipe/openpipe/assets/176426/48ad13fe-af2f-4294-bf32-62015597fd9b">
## Supported Models
- 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)
## Features
### 🔍 Visualize Responses
Inspect prompt completions side-by-side.
### 🧪 Bulk-Test
OpenPipe lets you _template_ a prompt. Use the templating feature to run the prompts you're testing against many potential inputs for broad coverage of your problem space.
### 📟 Translate between Model APIs
Write your prompt in one format and automatically convert it to work with any other model.
<img width="480" alt="Screenshot 2023-08-01 at 11 55 38 PM" src="https://github.com/OpenPipe/OpenPipe/assets/41524992/1e19ccf2-96b6-4e93-a3a5-1449710d1b5b" alt="translate between models">
<br><br>
### 🛠️ Refine Your Prompts Automatically
Use a growing database of best-practice refinements to improve your prompts automatically.
<img width="480" alt="Screenshot 2023-08-01 at 11 55 38 PM" src="https://github.com/OpenPipe/OpenPipe/assets/41524992/87a27fe7-daef-445c-a5e2-1c82b23f9f99" alt="add function call">
<br><br>
### 🪄 Auto-generate Test Scenarios
OpenPipe includes a tool to generate new test scenarios based on your existing prompts and scenarios. Just click "Autogenerate Scenario" to try it out!
<img width="600" src="https://github.com/openpipe/openpipe/assets/41524992/219a844e-3f4e-4f6b-8066-41348b42977b" alt="auto-generate">
<br><br>
## Running Locally
1. Install [Postgresql](https://www.postgresql.org/download/).
@@ -65,14 +55,7 @@ OpenPipe includes a tool to generate new test scenarios based on your existing p
4. Clone this repository: `git clone https://github.com/openpipe/openpipe`
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 migrate dev` to create the database.
7. Update `DATABASE_URL` if necessary to point to your Postgres instance and run `pnpm prisma db push` to create the database.
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)
## Testing Locally
1. Copy your `.env` file to `.env.test`.
2. Update the `DATABASE_URL` to have a different database name than your development one
3. Run `DATABASE_URL=[your new datatase url] pnpm prisma migrate dev --skip-seed --skip-generate`
4. Run `pnpm test`

49
app/.gitignore vendored
View File

@@ -1,49 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
# next.js
/.next/
/out/
next-env.d.ts
# production
/build
# misc
.DS_Store
*.pem
# debug
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.test
# vercel
.vercel
# typescript
*.tsbuildinfo
# Sentry Auth Token
.sentryclirc
# custom openai intialization
src/server/utils/openaiCustomConfig.json

View File

@@ -1,48 +0,0 @@
# Adapted from https://create.t3.gg/en/deployment/docker#3-create-dockerfile
FROM node:20.1.0-bullseye as base
RUN yarn global add pnpm
# DEPS
FROM base as deps
WORKDIR /code
COPY app/prisma app/package.json ./app/
COPY client-libs/typescript/package.json ./client-libs/typescript/
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
RUN cd app && pnpm install --frozen-lockfile
# BUILDER
FROM base as builder
# Include all NEXT_PUBLIC_* env vars here
ARG NEXT_PUBLIC_POSTHOG_KEY
ARG NEXT_PUBLIC_SOCKET_URL
ARG NEXT_PUBLIC_HOST
ARG NEXT_PUBLIC_SENTRY_DSN
ARG SENTRY_AUTH_TOKEN
ARG NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS
WORKDIR /code
COPY --from=deps /code/node_modules ./node_modules
COPY --from=deps /code/app/node_modules ./app/node_modules
COPY --from=deps /code/client-libs/typescript/node_modules ./client-libs/typescript/node_modules
COPY . .
RUN cd app && SKIP_ENV_VALIDATION=1 pnpm build
# RUNNER
FROM base as runner
WORKDIR /code/app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
COPY --from=builder /code/ /code/
EXPOSE 3000
ENV PORT 3000
# Run the "run-prod.sh" script
CMD /code/app/run-prod.sh

View File

@@ -1,63 +0,0 @@
import nextRoutes from "nextjs-routes/config";
import { withSentryConfig } from "@sentry/nextjs";
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
const { env } = await import("./src/env.mjs");
/** @type {import("next").NextConfig} */
let config = {
reactStrictMode: true,
/**
* If you have `experimental: { appDir: true }` set, then you must comment the below `i18n` config
* out.
*
* @see https://github.com/vercel/next.js/issues/41980
*/
i18n: {
locales: ["en"],
defaultLocale: "en",
},
rewrites: async () => [
{
source: "/ingest/:path*",
destination: "https://app.posthog.com/:path*",
},
],
webpack: (config) => {
config.module.rules.push({
test: /\.txt$/,
use: "raw-loader",
});
return config;
},
transpilePackages: ["openpipe"],
};
config = nextRoutes()(config);
if (env.NEXT_PUBLIC_SENTRY_DSN && env.SENTRY_AUTH_TOKEN) {
// @ts-expect-error - `withSentryConfig` is not typed correctly
config = withSentryConfig(
config,
{
authToken: env.SENTRY_AUTH_TOKEN,
silent: true,
org: "openpipe",
project: "openpipe",
},
{
widenClientFileUpload: true,
tunnelRoute: "/monitoring",
disableLogger: true,
},
);
}
export default config;

View File

@@ -1,7 +0,0 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "6.6.0"
}
}

View File

@@ -1,84 +0,0 @@
Text,sentiment,emotion
@dell your customer service is horrible especially agent syedfaisal who has made this experience of purchasing a new computer downright awful and Ill reconsider ever buying a Dell in the future @DellTech,negative,anger
@zacokalo @Dell @DellCares @Dell give the man what he paid for!,neutral,anger
"COOKING STREAM DAY!!! Ty to @Alienware for sponsoring this stream! Ill be making a bunch of Japanese Alien themed foods hehe
Come check it out! https://t.co/m06tJQ06zk
#alienwarepartner #intelgaming @Dell @IntelGaming https://t.co/qOdQX2E8VD",positive,joy
@emijuju_ @Alienware @Dell @intel Beautiful 😍❤️😻,positive,joy
"What's your biggest data management challenge? • Cloud complexity? • Lengthy tech refresh cycles? • Capital budget constraints? Solve your challenges with as-a-Storage. Get simplicity, agility &amp; control with @Dell #APEX. https://t.co/mCblMtH931 https://t.co/eepKNZ4Ai3",neutral,optimism
"This week we were at the ""Top Gun"" themed @Dell Product Expo. Eddie Muñoz met Maverick look-alike, California Tom Cruise (Jerome LeBlanc)!
""I feel the need, the need for speed."" - Maverick
#topgun #topgunmaverick #dell #delltechnologies #lockncharge https://t.co/QHYH2EbMjq",positive,joy
"Itsss been more than a week...i m following up with dell for troubleshootings...my https://t.co/lWhg2YKhQa suffering so as my hard earned money...hightly disappointed...contd..
@DellCares @Dell",negative,sadness
"@ashu_k7 @Dell Pathetic!!!!! I Dont mind taking legal action, this is deficency of service for which the customer is nt getting help..",negative,anger
@ashu_k7 @Dell Making life unhappy is the new tag line of #Dell,negative,sadness
"@Dell If you are buying a Dell, make sure you are making your life hell.
Better buy other laptops. If you wanted to opt for Dell better opt for garbage on the streets.",negative,anger
"MY DESK'S FINAL FORM? Seriously, I'm finally happy with my monitor setup here... and I'll keep this setup whenever I move... FOREVER. What do you think?
https://t.co/WJZ2JXtOnX
@Alienware @Dell cheers. https://t.co/6Whhldfpv0",positive,joy
"@Dell Dell Alienware computer has had software problems with SupportAssist since purchase. Dell, despite paying for Premium Support, has never fixed issues. Latest solution was to erase everything and reload....SupportAssist still doesn't work.",negative,anger
"HUGE congratulations to Startup Battle 3.0 winner ➡️ @Ox_Fulfillment x @cyborgcharu for being featured in @BusinessInsider &amp; @Dell showcasing the journey at Ox! 🚀🚀🚀
We love to see our portfolio companies continuing to BUILD SOMETHING FROM NOTHING! 🔥 https://t.co/awBkn5ippB",positive,joy
@Dell happy Friday!,positive,joy
"@intel Core i5 1135G7 - 4732 points
@intel Core i5 1235 - 6619 points
@Dell Latitude 5420 x 5430.
Cinebench R23. Good job Intel!",positive,joy
@Dell india we purchased 52 docking station and we have around 100 users using dell laptop as well as dell monitor now they are refusing to replace my faulty product and disconnecting my every call....,negative,anger
"It's another year ans another day But cant fill it in yet the child hood dreams.
It's my birthdy today. Can anyone of you guys bless me with a simplest gaming oc that can run
@DOTA2 ?
@Dell @HP @VastGG @Acer @Alienware @Lenovo @toshiba @IBM @Fujitsu_Global @NEC https://t.co/69G8tL9sN8",neutral,joy
"@idoccor @Dell That's always the decision—wait, or, look elsewhere. In this case, I think I unfortunately need to wait since there are only two monitors with these specs and I don't like the other one 😂",negative,sadness
"@MichaelDell @Dell @DellCares For how long this will continue. It is high time you either fix the problem for good or replace the complete laptop. Spent over 60+ hours with Customer Care teams, which is not helping. Cannot keep going on like this.",negative,anger
"@Dell @DellCares but no, not really",neutral,sadness
"Business innovation requires insight, agility and efficiency. How do you get there? RP PRO, LLC recommends starting by proactively managing IT infrastructure with #OpenManage Systems from @Dell. https://t.co/fBcK1lfFMu https://t.co/xWHLkkHCjn",neutral,optimism
@Dell Yessirrrrr #NationalCoffeeDay,positive,joy
"New blog post from @Dell shared on https://t.co/EgfPChB8AT
Re-routing Our Connected and Autonomous Future https://t.co/AW8EHQrbd6
#future #futuretech #techinnovation https://t.co/koX8stKPsr",neutral,joy
"In a free-market economy, the folks @IronMountain can set prices as they see fit. Their customers are also free to find better prices at competitors like @Dell
@H3CGlobal @HPE
https://t.co/reZ56DNTBI",neutral,optimism
"Delighted to chat with many of our partners here in person at @Intel Innovation! @Dell, @Lenovo, @Supermicro_SMCI, @QuantaQCT #IntelON https://t.co/BxIeGW8deN",positive,joy
"A special gracias to our Startup Chica San Antonio 2022 sponsors @eBay, @jcpenney, @Barbie, @HEB, @Dell, @Honda, @SouthsideSATX💜✨ https://t.co/lZ6WWkziHl",positive,joy
"When your team decides to start supporting developers, your #ops must change too. More from @cote and @Dell Developer Community Manager @barton808: https://t.co/W6f1oMiTgV",neutral,optimism
@EmDStowers @LASERGIANT1 @ohwormongod @Ludovician_Vega @Dell our boy snitchin,neutral,anger
A 1st place dmi:Design Value Award goes to @Dell for a packaging modernization initiative that helped them get closer to their corporate Moonshot Sustainability Goal of 100% recycled or renewable packaging by 2030. More at https://t.co/dnhZWWLCQC #designvalue #DVA22,positive,optimism
Reducing deployment and maintenance complexity is the goal behind @dell and @WindRiver's new collaboration. https://t.co/2PxQgPuHUU,positive,optimism
@jaserhunter @Dell Love the sales pitch lol,positive,joy
@Dell india we purchased 52 docking station and we have around 100 users using dell laptop as well as dell monitor now they are refusing to replace my faulty product and disconnecting my every call....,negative,anger
@ashu_k7 @Dell One more example.. their technical support is also worse. https://t.co/20atSgI4fg,negative,anger
*angry screeches about @Dell proprietary MBR windows 8.1 partitions not being able to save as an img in clonezilla *,negative,anger
@socialitebooks @BBYC_Gamers @Dell @Alienware @BestBuyCanada @intelcanada Congratulations!!!,positive,joy
"Thank you to the @dell team for coming out to volunteer today! We truly appreciate your hard work and look forward to seeing you again soon!
If you and your team are interested in helping out at the UMLAUF, visit our website for more information: https://t.co/lVfsZT2ogS https://t.co/eLz0FY0y4M",positive,joy
"@TheCaramelGamer @intel @bravadogaming @Intel_Africa @Dell @DellTech @DellTechMEA @Alienware @IntelUK we love to see it.
Also also actually actually whoever did that artwork? 🔥🔥🔥 am a fan.",positive,joy
"LOVING MY DELL 2 IN 1 LAPTOP
YAYY 🥳🥳
@Dell #DellInspiron #DellLaptop https://t.co/vib96jf3tC",positive,joy
@Azure @OracleItalia @AWS_Italy @lenovoitalia @Dell discussing the future of #HPC during the #hpcroundtable22 in Turin today #highperformancecomputing https://t.co/jJ1WqBulPF,neutral,joy
Attracting talent @AmericanChamber. @marg_cola @Dell speaks of quality of life connectivity and the Opportunity for development being so crucial. Housing availability is now impacting on decision making for potential candidates. #WhyCork,positive,optimism
.@Dell partners with @WindRiver on modular cloud-native telecommunications infrastructure https://t.co/4SWATspwCP @SiliconANGLE @Mike_Wheatley @holgermu @constellationr,neutral,joy
@Dell Not buy Dell Inspiron laptop,neutral,sadness
"@dell #delltechforum reminding us IDC have predicted that by 2024, 50% of everything we consume in technology will be as a service https://t.co/3UBiZJX0LE",neutral,optimism
@RachMurph @HETTShow @Dell Thank you for coming! Great evening,positive,joy
Congratulations to Jason M of Moncton NB on winning a @Dell @Alienware m15 R7 15.6″ gaming laptop from @BestBuyCanada and @intelcanada's gaming days #contest on the blog. Visit https://t.co/VryaY5Rvv9 to learn about tech and for chances to win new tech. https://t.co/T6n0dzF6oL,positive,joy
@MattVisiwig @Dell Sour taste for sure 😶 But don't let ego distract you from what you really want to buy 😁,neutral,optimism
"Massive thank you goes to sponsors @HendersonLoggie @lindsaysnews @Dell @unity, all of our fantastic judges and mentors and the team at @EGX and @ExCeLLondon.
Big congratulations also to all of our other @AbertayDare teams - an amazing year! #Dare2022 https://t.co/jYe4agO7lW",positive,joy
"@timetcetera @rahaug Nah, I just need @Dell to start paying me comissions 😂",neutral,joy
"""Whether youre an engineer, a designer, or work in supply chain management or sales, there are always opportunities to think about sustainability and how you can do things more efficiently."" 👏 — Oliver Campbell, Director of Packaging Engineering, @Dell https://t.co/vUJLTWNFwP https://t.co/GJWAzGfAxJ",positive,optimism
"Hi, my name is @listerepvp and I support @Dell, always.",positive,joy
1 Text sentiment emotion
2 @dell your customer service is horrible especially agent syedfaisal who has made this experience of purchasing a new computer downright awful and I’ll reconsider ever buying a Dell in the future @DellTech negative anger
3 @zacokalo @Dell @DellCares @Dell give the man what he paid for! neutral anger
4 COOKING STREAM DAY!!! Ty to @Alienware for sponsoring this stream! I’ll be making a bunch of Japanese Alien themed foods hehe Come check it out! https://t.co/m06tJQ06zk #alienwarepartner #intelgaming @Dell @IntelGaming https://t.co/qOdQX2E8VD positive joy
5 @emijuju_ @Alienware @Dell @intel Beautiful 😍❤️😻 positive joy
6 What's your biggest data management challenge? • Cloud complexity? • Lengthy tech refresh cycles? • Capital budget constraints? Solve your challenges with as-a-Storage. Get simplicity, agility &amp; control with @Dell #APEX. https://t.co/mCblMtH931 https://t.co/eepKNZ4Ai3 neutral optimism
7 This week we were at the "Top Gun" themed @Dell Product Expo. Eddie Muñoz met Maverick look-alike, California Tom Cruise (Jerome LeBlanc)! "I feel the need, the need for speed." - Maverick #topgun #topgunmaverick #dell #delltechnologies #lockncharge https://t.co/QHYH2EbMjq positive joy
8 Itsss been more than a week...i m following up with dell for troubleshootings...my https://t.co/lWhg2YKhQa suffering so as my hard earned money...hightly disappointed...contd.. @DellCares @Dell negative sadness
9 @ashu_k7 @Dell Pathetic!!!!! I Dont mind taking legal action, this is deficency of service for which the customer is nt getting help.. negative anger
10 @ashu_k7 @Dell Making life unhappy is the new tag line of #Dell negative sadness
11 @Dell If you are buying a Dell, make sure you are making your life hell. Better buy other laptops. If you wanted to opt for Dell better opt for garbage on the streets. negative anger
12 MY DESK'S FINAL FORM? Seriously, I'm finally happy with my monitor setup here... and I'll keep this setup whenever I move... FOREVER. What do you think? https://t.co/WJZ2JXtOnX @Alienware @Dell cheers. https://t.co/6Whhldfpv0 positive joy
13 @Dell Dell Alienware computer has had software problems with SupportAssist since purchase. Dell, despite paying for Premium Support, has never fixed issues. Latest solution was to erase everything and reload....SupportAssist still doesn't work. negative anger
14 HUGE congratulations to Startup Battle 3.0 winner ➡️ @Ox_Fulfillment x @cyborgcharu for being featured in @BusinessInsider &amp; @Dell showcasing the journey at Ox! 🚀🚀🚀 We love to see our portfolio companies continuing to BUILD SOMETHING FROM NOTHING! 🔥 https://t.co/awBkn5ippB positive joy
15 @Dell happy Friday! positive joy
16 @intel Core i5 1135G7 - 4732 points @intel Core i5 1235 - 6619 points @Dell Latitude 5420 x 5430. Cinebench R23. Good job Intel! positive joy
17 @Dell india we purchased 52 docking station and we have around 100 users using dell laptop as well as dell monitor now they are refusing to replace my faulty product and disconnecting my every call.... negative anger
18 It's another year ans another day But cant fill it in yet the child hood dreams. It's my birthdy today. Can anyone of you guys bless me with a simplest gaming oc that can run @DOTA2 ? @Dell @HP @VastGG @Acer @Alienware @Lenovo @toshiba @IBM @Fujitsu_Global @NEC https://t.co/69G8tL9sN8 neutral joy
19 @idoccor @Dell That's always the decision—wait, or, look elsewhere. In this case, I think I unfortunately need to wait since there are only two monitors with these specs and I don't like the other one 😂 negative sadness
20 @MichaelDell @Dell @DellCares For how long this will continue. It is high time you either fix the problem for good or replace the complete laptop. Spent over 60+ hours with Customer Care teams, which is not helping. Cannot keep going on like this. negative anger
21 @Dell @DellCares but no, not really neutral sadness
22 Business innovation requires insight, agility and efficiency. How do you get there? RP PRO, LLC recommends starting by proactively managing IT infrastructure with #OpenManage Systems from @Dell. https://t.co/fBcK1lfFMu https://t.co/xWHLkkHCjn neutral optimism
23 @Dell Yessirrrrr #NationalCoffeeDay positive joy
24 New blog post from @Dell shared on https://t.co/EgfPChB8AT Re-routing Our Connected and Autonomous Future https://t.co/AW8EHQrbd6 #future #futuretech #techinnovation https://t.co/koX8stKPsr neutral joy
25 In a free-market economy, the folks @IronMountain can set prices as they see fit. Their customers are also free to find better prices at competitors like @Dell @H3CGlobal @HPE https://t.co/reZ56DNTBI neutral optimism
26 Delighted to chat with many of our partners here in person at @Intel Innovation! @Dell, @Lenovo, @Supermicro_SMCI, @QuantaQCT #IntelON https://t.co/BxIeGW8deN positive joy
27 A special gracias to our Startup Chica San Antonio 2022 sponsors @eBay, @jcpenney, @Barbie, @HEB, @Dell, @Honda, @SouthsideSATX💜✨ https://t.co/lZ6WWkziHl positive joy
28 When your team decides to start supporting developers, your #ops must change too. More from @cote and @Dell Developer Community Manager @barton808: https://t.co/W6f1oMiTgV neutral optimism
29 @EmDStowers @LASERGIANT1 @ohwormongod @Ludovician_Vega @Dell our boy snitchin neutral anger
30 A 1st place dmi:Design Value Award goes to @Dell for a packaging modernization initiative that helped them get closer to their corporate Moonshot Sustainability Goal of 100% recycled or renewable packaging by 2030. More at https://t.co/dnhZWWLCQC #designvalue #DVA22 positive optimism
31 Reducing deployment and maintenance complexity is the goal behind @dell and @WindRiver's new collaboration. https://t.co/2PxQgPuHUU positive optimism
32 @jaserhunter @Dell Love the sales pitch lol positive joy
33 @Dell india we purchased 52 docking station and we have around 100 users using dell laptop as well as dell monitor now they are refusing to replace my faulty product and disconnecting my every call.... negative anger
34 @ashu_k7 @Dell One more example.. their technical support is also worse. https://t.co/20atSgI4fg negative anger
35 *angry screeches about @Dell proprietary MBR windows 8.1 partitions not being able to save as an img in clonezilla * negative anger
36 @socialitebooks @BBYC_Gamers @Dell @Alienware @BestBuyCanada @intelcanada Congratulations!!! positive joy
37 Thank you to the @dell team for coming out to volunteer today! We truly appreciate your hard work and look forward to seeing you again soon! If you and your team are interested in helping out at the UMLAUF, visit our website for more information: https://t.co/lVfsZT2ogS https://t.co/eLz0FY0y4M positive joy
38 @TheCaramelGamer @intel @bravadogaming @Intel_Africa @Dell @DellTech @DellTechMEA @Alienware @IntelUK we love to see it. Also also actually actually whoever did that artwork? 🔥🔥🔥 am a fan. positive joy
39 LOVING MY DELL 2 IN 1 LAPTOP YAYY 🥳🥳 @Dell #DellInspiron #DellLaptop https://t.co/vib96jf3tC positive joy
40 @Azure @OracleItalia @AWS_Italy @lenovoitalia @Dell discussing the future of #HPC during the #hpcroundtable22 in Turin today #highperformancecomputing https://t.co/jJ1WqBulPF neutral joy
41 Attracting talent @AmericanChamber. @marg_cola @Dell speaks of quality of life connectivity and the Opportunity for development being so crucial. Housing availability is now impacting on decision making for potential candidates. #WhyCork positive optimism
42 .@Dell partners with @WindRiver on modular cloud-native telecommunications infrastructure https://t.co/4SWATspwCP @SiliconANGLE @Mike_Wheatley @holgermu @constellationr neutral joy
43 @Dell Not buy Dell Inspiron laptop neutral sadness
44 @dell #delltechforum reminding us IDC have predicted that by 2024, 50% of everything we consume in technology will be as a service https://t.co/3UBiZJX0LE neutral optimism
45 @RachMurph @HETTShow @Dell Thank you for coming! Great evening positive joy
46 Congratulations to Jason M of Moncton NB on winning a @Dell @Alienware m15 R7 15.6″ gaming laptop from @BestBuyCanada and @intelcanada's gaming days #contest on the blog. Visit https://t.co/VryaY5Rvv9 to learn about tech and for chances to win new tech. https://t.co/T6n0dzF6oL positive joy
47 @MattVisiwig @Dell Sour taste for sure 😶 But don't let ego distract you from what you really want to buy 😁 neutral optimism
48 Massive thank you goes to sponsors @HendersonLoggie @lindsaysnews @Dell @unity, all of our fantastic judges and mentors and the team at @EGX and @ExCeLLondon. Big congratulations also to all of our other @AbertayDare teams - an amazing year! #Dare2022 https://t.co/jYe4agO7lW positive joy
49 @timetcetera @rahaug Nah, I just need @Dell to start paying me comissions 😂 neutral joy
50 "Whether you’re an engineer, a designer, or work in supply chain management or sales, there are always opportunities to think about sustainability and how you can do things more efficiently." 👏 — Oliver Campbell, Director of Packaging Engineering, @Dell https://t.co/vUJLTWNFwP https://t.co/GJWAzGfAxJ positive optimism
51 Hi, my name is @listerepvp and I support @Dell, always. positive joy

View File

@@ -1,16 +0,0 @@
-- CreateTable
CREATE TABLE "WorldChampEntrant" (
"id" UUID NOT NULL,
"userId" UUID NOT NULL,
"approved" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WorldChampEntrant_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "WorldChampEntrant_userId_key" ON "WorldChampEntrant"("userId");
-- AddForeignKey
ALTER TABLE "WorldChampEntrant" ADD CONSTRAINT "WorldChampEntrant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -1,5 +0,0 @@
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'USER');
-- AlterTable
ALTER TABLE "User" ADD COLUMN "role" "UserRole" NOT NULL DEFAULT 'USER';

View File

@@ -1,28 +0,0 @@
-- CreateTable
CREATE TABLE "Dataset" (
"id" UUID NOT NULL,
"name" TEXT NOT NULL,
"organizationId" UUID NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Dataset_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DatasetEntry" (
"id" UUID NOT NULL,
"input" TEXT NOT NULL,
"output" TEXT,
"datasetId" UUID NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DatasetEntry_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Dataset" ADD CONSTRAINT "Dataset_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DatasetEntry" ADD CONSTRAINT "DatasetEntry_datasetId_fkey" FOREIGN KEY ("datasetId") REFERENCES "Dataset"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,13 +0,0 @@
/*
Warnings:
- You are about to drop the column `constructFn` on the `PromptVariant` table. All the data in the column will be lost.
- You are about to drop the column `constructFnVersion` on the `PromptVariant` table. All the data in the column will be lost.
- Added the required column `promptConstructor` to the `PromptVariant` table without a default value. This is not possible if the table is not empty.
- Added the required column `promptConstructorVersion` to the `PromptVariant` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "PromptVariant" RENAME COLUMN "constructFn" TO "promptConstructor";
ALTER TABLE "PromptVariant" RENAME COLUMN "constructFnVersion" TO "promptConstructorVersion";

View File

@@ -1,90 +0,0 @@
-- CreateTable
CREATE TABLE "LoggedCall" (
"id" UUID NOT NULL,
"startTime" TIMESTAMP(3) NOT NULL,
"cacheHit" BOOLEAN NOT NULL,
"modelResponseId" UUID NOT NULL,
"organizationId" UUID NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "LoggedCall_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LoggedCallModelResponse" (
"id" UUID NOT NULL,
"reqPayload" JSONB NOT NULL,
"respStatus" INTEGER,
"respPayload" JSONB,
"error" TEXT,
"startTime" TIMESTAMP(3) NOT NULL,
"endTime" TIMESTAMP(3) NOT NULL,
"cacheKey" TEXT,
"durationMs" INTEGER,
"inputTokens" INTEGER,
"outputTokens" INTEGER,
"finishReason" TEXT,
"completionId" TEXT,
"totalCost" DECIMAL(18,12),
"originalLoggedCallId" UUID NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "LoggedCallModelResponse_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LoggedCallTag" (
"id" UUID NOT NULL,
"name" TEXT NOT NULL,
"value" TEXT,
"loggedCallId" UUID NOT NULL,
CONSTRAINT "LoggedCallTag_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ApiKey" (
"id" UUID NOT NULL,
"name" TEXT NOT NULL,
"apiKey" TEXT NOT NULL,
"organizationId" UUID NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "LoggedCall_startTime_idx" ON "LoggedCall"("startTime");
-- CreateIndex
CREATE UNIQUE INDEX "LoggedCallModelResponse_originalLoggedCallId_key" ON "LoggedCallModelResponse"("originalLoggedCallId");
-- CreateIndex
CREATE INDEX "LoggedCallModelResponse_cacheKey_idx" ON "LoggedCallModelResponse"("cacheKey");
-- CreateIndex
CREATE INDEX "LoggedCallTag_name_idx" ON "LoggedCallTag"("name");
-- CreateIndex
CREATE INDEX "LoggedCallTag_name_value_idx" ON "LoggedCallTag"("name", "value");
-- CreateIndex
CREATE UNIQUE INDEX "ApiKey_apiKey_key" ON "ApiKey"("apiKey");
-- AddForeignKey
ALTER TABLE "LoggedCall" ADD CONSTRAINT "LoggedCall_modelResponseId_fkey" FOREIGN KEY ("modelResponseId") REFERENCES "LoggedCallModelResponse"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LoggedCall" ADD CONSTRAINT "LoggedCall_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LoggedCallModelResponse" ADD CONSTRAINT "LoggedCallModelResponse_originalLoggedCallId_fkey" FOREIGN KEY ("originalLoggedCallId") REFERENCES "LoggedCall"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LoggedCallTag" ADD CONSTRAINT "LoggedCallTag_loggedCallId_fkey" FOREIGN KEY ("loggedCallId") REFERENCES "LoggedCall"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Organization" ADD COLUMN "name" TEXT NOT NULL DEFAULT 'Project 1';

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "LoggedCall" ALTER COLUMN "modelResponseId" DROP NOT NULL;

View File

@@ -1,37 +0,0 @@
-- Rename Enum
ALTER TYPE "OrganizationUserRole" RENAME TO "ProjectUserRole";
-- Drop and recreate foreign keys
ALTER TABLE "ApiKey" DROP CONSTRAINT "ApiKey_organizationId_fkey";
ALTER TABLE "Dataset" DROP CONSTRAINT "Dataset_organizationId_fkey";
ALTER TABLE "Experiment" DROP CONSTRAINT "Experiment_organizationId_fkey";
ALTER TABLE "LoggedCall" DROP CONSTRAINT "LoggedCall_organizationId_fkey";
ALTER TABLE "OrganizationUser" DROP CONSTRAINT "OrganizationUser_organizationId_fkey";
ALTER TABLE "OrganizationUser" DROP CONSTRAINT "OrganizationUser_userId_fkey";
-- Rename columns
ALTER TABLE "ApiKey" RENAME COLUMN "organizationId" TO "projectId";
ALTER TABLE "Dataset" RENAME COLUMN "organizationId" TO "projectId";
ALTER TABLE "Experiment" RENAME COLUMN "organizationId" TO "projectId";
ALTER TABLE "LoggedCall" RENAME COLUMN "organizationId" TO "projectId";
ALTER TABLE "OrganizationUser" RENAME COLUMN "organizationId" TO "projectId";
ALTER TABLE "Organization" RENAME COLUMN "personalOrgUserId" TO "personalProjectUserId";
-- Rename table
ALTER TABLE "Organization" RENAME TO "Project";
ALTER TABLE "OrganizationUser" RENAME TO "ProjectUser";
-- Recreate foreign keys
ALTER TABLE "Experiment" ADD CONSTRAINT "Experiment_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Dataset" ADD CONSTRAINT "Dataset_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ProjectUser" ADD CONSTRAINT "ProjectUser_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ProjectUser" ADD CONSTRAINT "ProjectUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "LoggedCall" ADD CONSTRAINT "LoggedCall_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Rename indexes
ALTER TABLE "Project" RENAME CONSTRAINT "Organization_pkey" TO "Project_pkey";
ALTER TABLE "ProjectUser" RENAME CONSTRAINT "OrganizationUser_pkey" TO "ProjectUser_pkey";
ALTER TABLE "Project" RENAME CONSTRAINT "Organization_personalOrgUserId_fkey" TO "Project_personalProjectUserId_fkey";
ALTER INDEX "Organization_personalOrgUserId_key" RENAME TO "Project_personalProjectUserId_key";
ALTER INDEX "OrganizationUser_organizationId_userId_key" RENAME TO "ProjectUser_projectId_userId_key";

View File

@@ -1 +0,0 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

View File

@@ -1,66 +0,0 @@
/*
Warnings:
- You are about to rename the column `completionTokens` to `outputTokens` on the `ModelResponse` table.
- You are about to rename the column `promptTokens` to `inputTokens` on the `ModelResponse` table.
- You are about to rename the column `startTime` on the `LoggedCall` table to `requestedAt`. Ensure compatibility with application logic.
- You are about to rename the column `startTime` on the `LoggedCallModelResponse` table to `requestedAt`. Ensure compatibility with application logic.
- You are about to rename the column `endTime` on the `LoggedCallModelResponse` table to `receivedAt`. Ensure compatibility with application logic.
- You are about to rename the column `error` on the `LoggedCallModelResponse` table to `errorMessage`. Ensure compatibility with application logic.
- You are about to rename the column `respStatus` on the `LoggedCallModelResponse` table to `statusCode`. Ensure compatibility with application logic.
- You are about to rename the column `totalCost` on the `LoggedCallModelResponse` table to `cost`. Ensure compatibility with application logic.
- You are about to rename the column `inputHash` on the `ModelResponse` table to `cacheKey`. Ensure compatibility with application logic.
- You are about to rename the column `output` on the `ModelResponse` table to `respPayload`. Ensure compatibility with application logic.
*/
-- DropIndex
DROP INDEX "LoggedCall_startTime_idx";
-- DropIndex
DROP INDEX "ModelResponse_inputHash_idx";
-- Rename completionTokens to outputTokens
ALTER TABLE "ModelResponse"
RENAME COLUMN "completionTokens" TO "outputTokens";
-- Rename promptTokens to inputTokens
ALTER TABLE "ModelResponse"
RENAME COLUMN "promptTokens" TO "inputTokens";
-- AlterTable
ALTER TABLE "LoggedCall"
RENAME COLUMN "startTime" TO "requestedAt";
-- AlterTable
ALTER TABLE "LoggedCallModelResponse"
RENAME COLUMN "startTime" TO "requestedAt";
-- AlterTable
ALTER TABLE "LoggedCallModelResponse"
RENAME COLUMN "endTime" TO "receivedAt";
-- AlterTable
ALTER TABLE "LoggedCallModelResponse"
RENAME COLUMN "error" TO "errorMessage";
-- AlterTable
ALTER TABLE "LoggedCallModelResponse"
RENAME COLUMN "respStatus" TO "statusCode";
-- AlterTable
ALTER TABLE "LoggedCallModelResponse"
RENAME COLUMN "totalCost" TO "cost";
-- AlterTable
ALTER TABLE "ModelResponse"
RENAME COLUMN "inputHash" TO "cacheKey";
-- AlterTable
ALTER TABLE "ModelResponse"
RENAME COLUMN "output" TO "respPayload";
-- CreateIndex
CREATE INDEX "LoggedCall_requestedAt_idx" ON "LoggedCall"("requestedAt");
-- CreateIndex
CREATE INDEX "ModelResponse_cacheKey_idx" ON "ModelResponse"("cacheKey");

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "LoggedCall" ADD COLUMN "model" TEXT;

View File

@@ -1,407 +0,0 @@
// 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)
projectId String @db.Uuid
project Project? @relation(fields: [projectId], 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
promptConstructor String
promptConstructorVersion 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
cacheKey String
requestedAt DateTime?
receivedAt DateTime?
respPayload Json?
cost Float?
inputTokens Int?
outputTokens 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[]
@@index([cacheKey])
}
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 Dataset {
id String @id @default(uuid()) @db.Uuid
name String
datasetEntries DatasetEntry[]
projectId String @db.Uuid
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model DatasetEntry {
id String @id @default(uuid()) @db.Uuid
input String
output String?
datasetId String @db.Uuid
dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Project {
id String @id @default(uuid()) @db.Uuid
name String @default("Project 1")
personalProjectUserId String? @unique @db.Uuid
personalProjectUser User? @relation(fields: [personalProjectUserId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
projectUsers ProjectUser[]
experiments Experiment[]
datasets Dataset[]
loggedCalls LoggedCall[]
apiKeys ApiKey[]
}
enum ProjectUserRole {
ADMIN
MEMBER
VIEWER
}
model ProjectUser {
id String @id @default(uuid()) @db.Uuid
role ProjectUserRole
projectId String @db.Uuid
project Project? @relation(fields: [projectId], 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([projectId, userId])
}
model WorldChampEntrant {
id String @id @default(uuid()) @db.Uuid
userId String @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
approved Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId])
}
model LoggedCall {
id String @id @default(uuid()) @db.Uuid
requestedAt DateTime
// True if this call was served from the cache, false otherwise
cacheHit Boolean
// A LoggedCall is always associated with a LoggedCallModelResponse. If this
// is a cache miss, we create a new LoggedCallModelResponse.
// If it's a cache hit, it's a pre-existing LoggedCallModelResponse.
modelResponseId String? @db.Uuid
modelResponse LoggedCallModelResponse? @relation(fields: [modelResponseId], references: [id], onDelete: Cascade)
// The responses created by this LoggedCall. Will be empty if this LoggedCall was a cache hit.
createdResponses LoggedCallModelResponse[] @relation(name: "ModelResponseOriginalCall")
projectId String @db.Uuid
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
model String?
tags LoggedCallTag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([requestedAt])
}
model LoggedCallModelResponse {
id String @id @default(uuid()) @db.Uuid
reqPayload Json
// The HTTP status returned by the model provider
statusCode Int?
respPayload Json?
// Should be null if the request was successful, and some string if the request failed.
errorMessage String?
requestedAt DateTime
receivedAt DateTime
// Note: the function to calculate the cacheKey should include the project
// ID so we don't share cached responses between projects, which could be an
// attack vector. Also, we should only set the cacheKey on the model if the
// request was successful.
cacheKey String?
// Derived fields
durationMs Int?
inputTokens Int?
outputTokens Int?
finishReason String?
completionId String?
cost Decimal? @db.Decimal(18, 12)
// The LoggedCall that created this LoggedCallModelResponse
originalLoggedCallId String @unique @db.Uuid
originalLoggedCall LoggedCall @relation(name: "ModelResponseOriginalCall", fields: [originalLoggedCallId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
loggedCalls LoggedCall[]
@@index([cacheKey])
}
model LoggedCallTag {
id String @id @default(uuid()) @db.Uuid
name String
value String?
loggedCallId String @db.Uuid
loggedCall LoggedCall @relation(fields: [loggedCallId], references: [id], onDelete: Cascade)
@@index([name])
@@index([name, value])
}
model ApiKey {
id String @id @default(uuid()) @db.Uuid
name String
apiKey String @unique
projectId String @db.Uuid
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
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)
}
enum UserRole {
ADMIN
USER
}
model User {
id String @id @default(uuid()) @db.Uuid
name String?
email String? @unique
emailVerified DateTime?
image String?
role UserRole @default(USER)
accounts Account[]
sessions Session[]
projectUsers ProjectUser[]
projects Project[]
worldChampEntrant WorldChampEntrant?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}

View File

@@ -1,128 +0,0 @@
import { prisma } from "~/server/db";
import { generateNewCell } from "~/server/utils/generateNewCell";
import dedent from "dedent";
import { execSync } from "child_process";
import fs from "fs";
import { promptConstructorVersion } from "~/promptConstructor/version";
const defaultId = "11111111-1111-1111-1111-111111111112";
await prisma.project.deleteMany({
where: { id: defaultId },
});
// If there's an existing project, just seed into it
const project =
(await prisma.project.findFirst({})) ??
(await prisma.project.create({
data: { id: defaultId },
}));
// Clone the repo from git@github.com:microsoft/AGIEval.git into a tmp dir if it doesn't exist
const tmpDir = "/tmp/agi-eval";
if (!fs.existsSync(tmpDir)) {
execSync(`git clone git@github.com:microsoft/AGIEval.git ${tmpDir}`);
}
const datasets = [
"sat-en",
"sat-math",
"lsat-rc",
"lsat-ar",
"aqua-rat",
"logiqa-en",
"lsat-lr",
"math",
];
type Scenario = {
passage: string | null;
question: string;
options: string[] | null;
label: string;
};
for (const dataset of datasets) {
const experimentName = `AGI-Eval: ${dataset}`;
const oldExperiment = await prisma.experiment.findFirst({
where: {
label: experimentName,
projectId: project.id,
},
});
if (oldExperiment) {
await prisma.experiment.deleteMany({
where: { id: oldExperiment.id },
});
}
const experiment = await prisma.experiment.create({
data: {
id: oldExperiment?.id ?? undefined,
label: experimentName,
projectId: project.id,
},
});
const scenarios: Scenario[] = fs
.readFileSync(`${tmpDir}/data/v1/${dataset}.jsonl`, "utf8")
.split("\n")
.filter((line) => line.length > 0)
.map((line) => JSON.parse(line) as Scenario);
console.log("scenarios", scenarios.length);
await prisma.testScenario.createMany({
data: scenarios.slice(0, 30).map((scenario, i) => ({
experimentId: experiment.id,
sortIndex: i,
variableValues: {
passage: scenario.passage,
question: scenario.question,
options: scenario.options?.join("\n"),
label: scenario.label,
},
})),
});
await prisma.templateVariable.createMany({
data: ["passage", "question", "options", "label"].map((label) => ({
experimentId: experiment.id,
label,
})),
});
await prisma.promptVariant.createMany({
data: [
{
experimentId: experiment.id,
label: "Prompt Variant 1",
sortIndex: 0,
model: "gpt-3.5-turbo-0613",
modelProvider: "openai/ChatCompletion",
promptConstructorVersion,
promptConstructor: dedent`
definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo-0613",
messages: [
{
role: "user",
content: \`Passage: ${"$"}{scenario.passage}\n\nQuestion: ${"$"}{scenario.question}\n\nOptions: ${"$"}{scenario.options}\n\n Respond with just the letter of the best option in the format Answer: (A).\`
}
],
temperature: 0,
})`,
},
],
});
await prisma.evaluation.createMany({
data: [
{
experimentId: experiment.id,
label: "Eval",
evalType: "CONTAINS",
value: "Answer: ({{label}})",
},
],
});
}

File diff suppressed because one or more lines are too long

View File

@@ -1,114 +0,0 @@
import { prisma } from "~/server/db";
import dedent from "dedent";
import fs from "fs";
import { parse } from "csv-parse/sync";
import { promptConstructorVersion } from "~/promptConstructor/version";
const defaultId = "11111111-1111-1111-1111-111111111112";
await prisma.project.deleteMany({
where: { id: defaultId },
});
// If there's an existing project, just seed into it
const project =
(await prisma.project.findFirst({})) ??
(await prisma.project.create({
data: { id: defaultId },
}));
type Scenario = {
text: string;
sentiment: string;
emotion: string;
};
const experimentName = `Twitter Sentiment Analysis`;
const oldExperiment = await prisma.experiment.findFirst({
where: {
label: experimentName,
projectId: project.id,
},
});
if (oldExperiment) {
await prisma.experiment.deleteMany({
where: { id: oldExperiment.id },
});
}
const experiment = await prisma.experiment.create({
data: {
id: oldExperiment?.id ?? undefined,
label: experimentName,
projectId: project.id,
},
});
const content = fs.readFileSync("./prisma/datasets/validated_tweets.csv", "utf8");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const records: any[] = parse(content, { delimiter: ",", from_line: 2 });
console.log("records", records);
const scenarios: Scenario[] = records.map((row) => ({
text: row[0],
sentiment: row[1],
emotion: row[2],
}));
console.log("scenarios", scenarios.length);
await prisma.testScenario.createMany({
data: scenarios.slice(0, 30).map((scenario, i) => ({
experimentId: experiment.id,
sortIndex: i,
variableValues: {
text: scenario.text,
sentiment: scenario.sentiment,
emotion: scenario.emotion,
},
})),
});
await prisma.templateVariable.createMany({
data: ["text", "sentiment", "emotion"].map((label) => ({
experimentId: experiment.id,
label,
})),
});
await prisma.promptVariant.createMany({
data: [
{
experimentId: experiment.id,
label: "Prompt Variant 1",
sortIndex: 0,
model: "gpt-3.5-turbo-0613",
modelProvider: "openai/ChatCompletion",
promptConstructorVersion,
promptConstructor: dedent`
definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo-0613",
messages: [
{
role: "user",
content: \`Text: ${"$"}{scenario.text}\n\nRespond with the sentiment (negative|neutral|positive) and emotion (optimism|joy|anger|sadness) of the tweet in this format: "answer: <sentiment>-<emotion>".\`
}
],
temperature: 0,
})`,
},
],
});
await prisma.evaluation.createMany({
data: [
{
experimentId: experiment.id,
label: "Eval",
evalType: "CONTAINS",
value: "answer: {{sentiment}}-{{emotion}}",
},
],
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,28 +0,0 @@
<svg width="398" height="550" viewBox="0 0 398 550" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M39 125H359V542C359 546.418 355.418 550 351 550H47C42.5817 550 39 546.418 39 542V125Z" fill="black"/>
<path d="M0 8C0 3.58172 3.58172 0 8 0H390C394.418 0 398 3.58172 398 8V127C398 131.418 394.418 135 390 135H7.99999C3.58171 135 0 131.418 0 127V8Z" fill="black"/>
<path d="M50 135H348V535C348 537.209 346.209 539 344 539H54C51.7909 539 50 537.209 50 535V135Z" fill="#FF5733"/>
<path d="M11 14.0001C11 11.791 12.7909 10.0001 15 10.0001H384C386.209 10.0001 388 11.791 388 14.0001V120C388 122.209 386.209 124 384 124H15C12.7909 124 11 122.209 11 120V14.0001Z" fill="#FF5733"/>
<path d="M11 14.0001C11 11.791 12.7909 10.0001 15 10.0001H384C386.209 10.0001 388 11.791 388 14.0001V120C388 122.209 386.209 124 384 124H15C12.7909 124 11 122.209 11 120V14.0001Z" fill="url(#paint0_linear_102_49)"/>
<path d="M50 134H348V535C348 537.209 346.209 539 344 539H54C51.7909 539 50 537.209 50 535V134Z" fill="url(#paint1_linear_102_49)"/>
<path d="M108 142H156V535H108V142Z" fill="white"/>
<path d="M300 135H348V535C348 537.209 346.209 539 344 539H300V135Z" fill="white" fill-opacity="0.25"/>
<path d="M96 142H108V535H96V142Z" fill="white" fill-opacity="0.5"/>
<path d="M84 10.0001H133V120H84V10.0001Z" fill="white"/>
<path d="M339 10.0001H384C386.209 10.0001 388 11.791 388 14.0001V120C388 122.209 386.209 124 384 124H339V10.0001Z" fill="white" fill-opacity="0.25"/>
<path d="M71.9995 10.0001H83.9995V120H71.9995V10.0001Z" fill="white" fill-opacity="0.5"/>
<path d="M108 534.529H156V539.019H108V534.529Z" fill="#AAAAAA"/>
<path opacity="0.5" d="M95.9927 534.529H107.982V539.019H95.9927V534.529Z" fill="#AAAAAA"/>
<path d="M84.0029 119.887H133.007V124.027H84.0029V119.887Z" fill="#AAAAAA"/>
<path opacity="0.5" d="M71.9883 119.887H83.978V124.027H71.9883V119.887Z" fill="#AAAAAA"/>
<defs>
<linearGradient id="paint0_linear_102_49" x1="335" y1="67.0001" x2="137" y2="67.0001" gradientUnits="userSpaceOnUse">
<stop stop-color="#D62600"/>
<stop offset="1" stop-color="#FF5733" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_102_49" x1="306.106" y1="336.5" x2="149.597" y2="336.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#D62600"/>
<stop offset="1" stop-color="#FF5733" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -1,33 +0,0 @@
// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
import { env } from "~/env.mjs";
if (env.NEXT_PUBLIC_SENTRY_DSN) {
Sentry.init({
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
replaysOnErrorSampleRate: 1.0,
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
integrations: [
new Sentry.Replay({
// Additional Replay configuration goes in here, for example:
maskAllText: true,
blockAllMedia: true,
}),
],
});
}

View File

@@ -1,19 +0,0 @@
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
// The config you add here will be used whenever one of the edge features is loaded.
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
import { env } from "~/env.mjs";
if (env.NEXT_PUBLIC_SENTRY_DSN) {
Sentry.init({
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});
}

View File

@@ -1,18 +0,0 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
import { env } from "~/env.mjs";
if (env.NEXT_PUBLIC_SENTRY_DSN) {
Sentry.init({
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});
}

View File

@@ -1,40 +0,0 @@
import { HStack, Icon, IconButton, Tooltip, Text } from "@chakra-ui/react";
import { useState } from "react";
import { MdContentCopy } from "react-icons/md";
import { useHandledAsyncCallback } from "~/utils/hooks";
const CopiableCode = ({ code }: { code: string }) => {
const [copied, setCopied] = useState(false);
const [copyToClipboard] = useHandledAsyncCallback(async () => {
await navigator.clipboard.writeText(code);
setCopied(true);
}, [code]);
return (
<HStack
backgroundColor="blackAlpha.800"
color="white"
borderRadius={4}
padding={3}
w="full"
justifyContent="space-between"
>
<Text fontFamily="inconsolata" fontWeight="bold" letterSpacing={0.5} overflowX="auto">
{code}
</Text>
<Tooltip closeOnClick={false} label={copied ? "Copied!" : "Copy to clipboard"}>
<IconButton
aria-label="Copy"
icon={<Icon as={MdContentCopy} boxSize={5} />}
size="xs"
colorScheme="white"
variant="ghost"
onClick={copyToClipboard}
onMouseLeave={() => setCopied(false)}
/>
</Tooltip>
</HStack>
);
};
export default CopiableCode;

View File

@@ -1,185 +0,0 @@
import { Text, Button, HStack, Heading, Icon, IconButton, Stack, VStack } from "@chakra-ui/react";
import { type TemplateVariable } from "@prisma/client";
import { useEffect, useState } from "react";
import { BsPencil, BsX } from "react-icons/bs";
import { api } from "~/utils/api";
import { useExperiment, useHandledAsyncCallback, useScenarioVars } from "~/utils/hooks";
import { maybeReportError } from "~/utils/errorHandling/maybeReportError";
import { FloatingLabelInput } from "./FloatingLabelInput";
export const ScenarioVar = ({
variable,
isEditing,
setIsEditing,
}: {
variable: Pick<TemplateVariable, "id" | "label">;
isEditing: boolean;
setIsEditing: (isEditing: boolean) => void;
}) => {
const utils = api.useContext();
const [label, setLabel] = useState(variable.label);
useEffect(() => {
setLabel(variable.label);
}, [variable.label]);
const renameVarMutation = api.scenarioVars.rename.useMutation();
const [onRename] = useHandledAsyncCallback(async () => {
const resp = await renameVarMutation.mutateAsync({ id: variable.id, label });
if (maybeReportError(resp)) return;
setIsEditing(false);
await utils.scenarioVars.list.invalidate();
await utils.scenarios.list.invalidate();
}, [label, variable.id]);
const deleteMutation = api.scenarioVars.delete.useMutation();
const [onDeleteVar] = useHandledAsyncCallback(async () => {
await deleteMutation.mutateAsync({ id: variable.id });
await utils.scenarioVars.list.invalidate();
}, [variable.id]);
if (isEditing) {
return (
<HStack w="full">
<FloatingLabelInput
flex={1}
label="Renamed Variable"
value={label}
onChange={(e) => setLabel(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onRename();
}
// If the user types a space, replace it with an underscore
if (e.key === " ") {
e.preventDefault();
setLabel((label) => label && `${label}_`);
}
}}
/>
<Button size="sm" onClick={() => setIsEditing(false)}>
Cancel
</Button>
<Button size="sm" colorScheme="blue" onClick={onRename}>
Save
</Button>
</HStack>
);
} else {
return (
<HStack w="full" borderTopWidth={1} borderColor="gray.200">
<Text flex={1}>{variable.label}</Text>
<IconButton
aria-label="Edit"
variant="unstyled"
minW="unset"
color="gray.400"
onClick={() => setIsEditing(true)}
_hover={{ color: "gray.800", cursor: "pointer" }}
icon={<Icon as={BsPencil} />}
/>
<IconButton
aria-label="Delete"
variant="unstyled"
minW="unset"
color="gray.400"
onClick={onDeleteVar}
_hover={{ color: "gray.800", cursor: "pointer" }}
icon={<Icon as={BsX} boxSize={6} />}
/>
</HStack>
);
}
};
export default function EditScenarioVars() {
const experiment = useExperiment();
const vars = useScenarioVars();
const [currentlyEditingId, setCurrentlyEditingId] = useState<string | null>(null);
const [newVariable, setNewVariable] = useState<string>("");
const newVarIsValid = newVariable?.length ?? 0 > 0;
const utils = api.useContext();
const addVarMutation = api.scenarioVars.create.useMutation();
const [onAddVar] = useHandledAsyncCallback(async () => {
if (!experiment.data?.id) return;
if (!newVariable) return;
const resp = await addVarMutation.mutateAsync({
experimentId: experiment.data.id,
label: newVariable,
});
if (maybeReportError(resp)) return;
await utils.scenarioVars.list.invalidate();
setNewVariable("");
}, [addVarMutation, experiment.data?.id, newVarIsValid, newVariable]);
return (
<Stack>
<Heading size="sm">Scenario Variables</Heading>
<VStack spacing={4}>
<Text fontSize="sm">
Scenario variables can be used in your prompt variants as well as evaluations.
</Text>
<VStack spacing={0} w="full">
{vars.data?.map((variable) => (
<ScenarioVar
variable={variable}
key={variable.id}
isEditing={currentlyEditingId === variable.id}
setIsEditing={(isEditing) => {
if (isEditing) {
setCurrentlyEditingId(variable.id);
} else {
setCurrentlyEditingId(null);
}
}}
/>
))}
</VStack>
{currentlyEditingId !== "new" && (
<Button
colorScheme="blue"
size="sm"
onClick={() => setCurrentlyEditingId("new")}
alignSelf="end"
>
New Variable
</Button>
)}
{currentlyEditingId === "new" && (
<HStack w="full">
<FloatingLabelInput
flex={1}
label="New Variable"
value={newVariable}
onChange={(e) => setNewVariable(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onAddVar();
}
// If the user types a space, replace it with an underscore
if (e.key === " ") {
e.preventDefault();
setNewVariable((v) => v && `${v}_`);
}
}}
/>
<Button size="sm" onClick={() => setCurrentlyEditingId(null)}>
Cancel
</Button>
<Button size="sm" colorScheme="blue" onClick={onAddVar}>
Save
</Button>
</HStack>
)}
</VStack>
</Stack>
);
}

View File

@@ -1,36 +0,0 @@
import {
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
type UseDisclosureReturn,
} from "@chakra-ui/react";
import { type RouterOutputs } from "~/utils/api";
import { JSONTree } from "react-json-tree";
export default function ExpandedModal(props: {
cell: NonNullable<RouterOutputs["scenarioVariantCells"]["get"]>;
disclosure: UseDisclosureReturn;
}) {
return (
<Modal isOpen={props.disclosure.isOpen} onClose={props.disclosure.onClose} size="2xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>Prompt</ModalHeader>
<ModalCloseButton />
<ModalBody>
<JSONTree
data={props.cell.prompt}
invertTheme={true}
theme="chalk"
shouldExpandNodeInitially={() => true}
getItemString={() => ""}
hideRoot
/>
</ModalBody>
</ModalContent>
</Modal>
);
}

View File

@@ -1,53 +0,0 @@
import { HStack, Icon, IconButton, Spinner, Tooltip, useDisclosure } from "@chakra-ui/react";
import { BsArrowClockwise, BsInfoCircle } from "react-icons/bs";
import { useExperimentAccess } from "~/utils/hooks";
import ExpandedModal from "./PromptModal";
import { type RouterOutputs } from "~/utils/api";
export const CellOptions = ({
cell,
refetchingOutput,
refetchOutput,
}: {
cell: RouterOutputs["scenarioVariantCells"]["get"];
refetchingOutput: boolean;
refetchOutput: () => void;
}) => {
const { canModify } = useExperimentAccess();
const modalDisclosure = useDisclosure();
return (
<HStack justifyContent="flex-end" w="full" spacing={1}>
{cell && (
<>
<Tooltip label="See Prompt">
<IconButton
aria-label="See Prompt"
icon={<Icon as={BsInfoCircle} boxSize={3.5} />}
onClick={modalDisclosure.onOpen}
size="xs"
colorScheme="gray"
color="gray.500"
variant="ghost"
/>
</Tooltip>
<ExpandedModal cell={cell} disclosure={modalDisclosure} />
</>
)}
{canModify && (
<Tooltip label="Refetch output">
<IconButton
size="xs"
color="gray.500"
variant="ghost"
cursor="pointer"
onClick={refetchOutput}
aria-label="refetch output"
icon={<Icon as={refetchingOutput ? Spinner : BsArrowClockwise} boxSize={4} />}
/>
</Tooltip>
)}
</HStack>
);
};

View File

@@ -1,206 +0,0 @@
import { isEqual } from "lodash-es";
import { useEffect, useState, type DragEvent } from "react";
import { api } from "~/utils/api";
import { useExperimentAccess, useHandledAsyncCallback, useScenarioVars } from "~/utils/hooks";
import { type Scenario } from "./types";
import {
Box,
Button,
HStack,
Icon,
IconButton,
Spinner,
Text,
Tooltip,
VStack,
} from "@chakra-ui/react";
import { BsArrowsAngleExpand, BsX } from "react-icons/bs";
import { cellPadding } from "../constants";
import { FloatingLabelInput } from "./FloatingLabelInput";
import { ScenarioEditorModal } from "./ScenarioEditorModal";
export default function ScenarioEditor({
scenario,
...props
}: {
scenario: Scenario;
hovered: boolean;
canHide: boolean;
}) {
const { canModify } = useExperimentAccess();
const savedValues = scenario.variableValues as Record<string, string>;
const utils = api.useContext();
const [isDragTarget, setIsDragTarget] = useState(false);
const [variableInputHovered, setVariableInputHovered] = useState(false);
const [values, setValues] = useState<Record<string, string>>(savedValues);
useEffect(() => {
if (savedValues) setValues(savedValues);
}, [savedValues]);
const vars = useScenarioVars();
const variableLabels = vars.data?.map((v) => v.label) ?? [];
const hasChanged = !isEqual(savedValues, values);
const mutation = api.scenarios.replaceWithValues.useMutation();
const [onSave] = useHandledAsyncCallback(async () => {
await mutation.mutateAsync({
id: scenario.id,
values,
});
await utils.scenarios.list.invalidate();
}, [mutation, values]);
const hideMutation = api.scenarios.hide.useMutation();
const [onHide, hidingInProgress] = useHandledAsyncCallback(async () => {
await hideMutation.mutateAsync({
id: scenario.id,
});
await utils.scenarios.list.invalidate();
await utils.promptVariants.stats.invalidate();
}, [hideMutation, scenario.id]);
const reorderMutation = api.scenarios.reorder.useMutation();
const [onReorder] = useHandledAsyncCallback(
async (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragTarget(false);
const draggedId = e.dataTransfer.getData("text/plain");
const droppedId = scenario.id;
if (!draggedId || !droppedId || draggedId === droppedId) return;
await reorderMutation.mutateAsync({
draggedId,
droppedId,
});
await utils.scenarios.list.invalidate();
},
[reorderMutation, scenario.id],
);
const [scenarioEditorModalOpen, setScenarioEditorModalOpen] = useState(false);
return (
<>
<HStack
alignItems="flex-start"
px={cellPadding.x}
py={cellPadding.y}
spacing={0}
height="100%"
draggable={!variableInputHovered}
onDragStart={(e) => {
e.dataTransfer.setData("text/plain", scenario.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.100" : "transparent"}
>
{variableLabels.length === 0 ? (
<Box color="gray.500">
{vars.data ? "No scenario variables configured" : "Loading..."}
</Box>
) : (
<VStack spacing={4} flex={1} py={2}>
<HStack justifyContent="space-between" w="100%" align="center" spacing={0}>
<Text flex={1}>Scenario</Text>
<Tooltip label="Expand" hasArrow>
<IconButton
aria-label="Expand"
icon={<Icon as={BsArrowsAngleExpand} boxSize={3} />}
onClick={() => setScenarioEditorModalOpen(true)}
size="xs"
colorScheme="gray"
color="gray.500"
variant="ghost"
/>
</Tooltip>
{canModify && props.canHide && (
<Tooltip label="Delete" hasArrow>
<IconButton
aria-label="Delete"
icon={
<Icon
as={hidingInProgress ? Spinner : BsX}
boxSize={hidingInProgress ? 4 : 6}
/>
}
onClick={onHide}
size="xs"
display="flex"
colorScheme="gray"
color="gray.500"
variant="ghost"
/>
</Tooltip>
)}
</HStack>
{variableLabels.map((key) => {
const value = values[key] ?? "";
return (
<FloatingLabelInput
key={key}
label={key}
isDisabled={!canModify}
style={{ width: "100%" }}
maxHeight={32}
value={value}
onChange={(e) => {
setValues((prev) => ({ ...prev, [key]: e.target.value }));
}}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
e.currentTarget.blur();
onSave();
}
}}
onMouseEnter={() => setVariableInputHovered(true)}
onMouseLeave={() => setVariableInputHovered(false)}
/>
);
})}
{hasChanged && (
<HStack justify="right">
<Button
size="sm"
onMouseDown={() => {
setValues(savedValues);
}}
colorScheme="gray"
>
Reset
</Button>
<Button size="sm" onMouseDown={onSave} colorScheme="blue">
Save
</Button>
</HStack>
)}
</VStack>
)}
</HStack>
{scenarioEditorModalOpen && (
<ScenarioEditorModal
scenarioId={scenario.id}
initialValues={savedValues}
onClose={() => setScenarioEditorModalOpen(false)}
/>
)}
</>
);
}

View File

@@ -1,123 +0,0 @@
import {
Button,
HStack,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Spinner,
Text,
VStack,
} from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { isEqual } from "lodash-es";
import { api } from "~/utils/api";
import {
useScenario,
useHandledAsyncCallback,
useExperiment,
useExperimentAccess,
} from "~/utils/hooks";
import { FloatingLabelInput } from "./FloatingLabelInput";
export const ScenarioEditorModal = ({
scenarioId,
initialValues,
onClose,
}: {
scenarioId: string;
initialValues: Record<string, string>;
onClose: () => void;
}) => {
const utils = api.useContext();
const experiment = useExperiment();
const { canModify } = useExperimentAccess();
const scenario = useScenario(scenarioId);
const savedValues = scenario.data?.variableValues as Record<string, string>;
const [values, setValues] = useState<Record<string, string>>(initialValues);
useEffect(() => {
if (savedValues) setValues(savedValues);
}, [savedValues]);
const hasChanged = !isEqual(savedValues, values);
const mutation = api.scenarios.replaceWithValues.useMutation();
const [onSave, saving] = useHandledAsyncCallback(async () => {
await mutation.mutateAsync({
id: scenarioId,
values,
});
await utils.scenarios.list.invalidate();
}, [mutation, values]);
const vars = api.scenarioVars.list.useQuery({ experimentId: experiment.data?.id ?? "" });
const variableLabels = vars.data?.map((v) => v.label) ?? [];
return (
<Modal
isOpen
onClose={onClose}
size={{ base: "xl", sm: "2xl", md: "3xl", lg: "5xl", xl: "7xl" }}
>
<ModalOverlay />
<ModalContent w={1200}>
<ModalHeader />
<ModalCloseButton />
<ModalBody maxW="unset">
<VStack spacing={8}>
{values &&
variableLabels.map((key) => {
const value = values[key] ?? "";
return (
<FloatingLabelInput
key={key}
label={key}
isDisabled={!canModify}
_disabled={{ opacity: 1 }}
style={{ width: "100%" }}
value={value}
onChange={(e) => {
setValues((prev) => ({ ...prev, [key]: e.target.value }));
}}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
e.currentTarget.blur();
onSave();
}
}}
/>
);
})}
</VStack>
</ModalBody>
<ModalFooter>
{canModify && (
<HStack>
<Button
colorScheme="gray"
onClick={() => setValues(savedValues)}
minW={24}
isDisabled={!hasChanged}
>
<Text>Reset</Text>
</Button>
<Button colorScheme="blue" onClick={onSave} minW={24} isDisabled={!hasChanged}>
{saving ? <Spinner boxSize={4} /> : <Text>Save</Text>}
</Button>
</HStack>
)}
</ModalFooter>
</ModalContent>
</Modal>
);
};

View File

@@ -1,16 +0,0 @@
import { type StackProps } from "@chakra-ui/react";
import { useScenarios } from "~/utils/hooks";
import Paginator from "../Paginator";
const ScenarioPaginator = (props: StackProps) => {
const { data } = useScenarios();
if (!data) return null;
const { count } = data;
return <Paginator count={count} condense {...props} />;
};
export default ScenarioPaginator;

View File

@@ -1,119 +0,0 @@
import { HStack, IconButton, Text, Select, type StackProps, Icon } from "@chakra-ui/react";
import React, { useCallback } from "react";
import { FiChevronsLeft, FiChevronsRight, FiChevronLeft, FiChevronRight } from "react-icons/fi";
import { usePageParams } from "~/utils/hooks";
const pageSizeOptions = [10, 25, 50, 100];
const Paginator = ({
count,
condense,
...props
}: { count: number; condense?: boolean } & StackProps) => {
const { page, pageSize, setPageParams } = usePageParams();
const lastPage = Math.ceil(count / pageSize);
const updatePageSize = useCallback(
(newPageSize: number) => {
const newPage = Math.floor(((page - 1) * pageSize) / newPageSize) + 1;
setPageParams({ page: newPage, pageSize: newPageSize }, "replace");
},
[page, pageSize, setPageParams],
);
const nextPage = () => {
if (page < lastPage) {
setPageParams({ page: page + 1 }, "replace");
}
};
const prevPage = () => {
if (page > 1) {
setPageParams({ page: page - 1 }, "replace");
}
};
const goToLastPage = () => setPageParams({ page: lastPage }, "replace");
const goToFirstPage = () => setPageParams({ page: 1 }, "replace");
return (
<HStack
pt={4}
spacing={8}
justifyContent={condense ? "flex-start" : "space-between"}
alignItems="center"
w="full"
{...props}
>
{!condense && (
<>
<HStack>
<Text>Rows</Text>
<Select
value={pageSize}
onChange={(e) => updatePageSize(parseInt(e.target.value))}
w={20}
backgroundColor="white"
>
{pageSizeOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</Select>
</HStack>
<Text>
Page {page} of {lastPage}
</Text>
</>
)}
<HStack>
<IconButton
variant="outline"
size="sm"
onClick={goToFirstPage}
isDisabled={page === 1}
aria-label="Go to first page"
icon={<Icon as={FiChevronsLeft} boxSize={5} strokeWidth={1.5} />}
bgColor="white"
/>
<IconButton
variant="outline"
size="sm"
onClick={prevPage}
isDisabled={page === 1}
aria-label="Previous page"
icon={<Icon as={FiChevronLeft} boxSize={5} strokeWidth={1.5} />}
bgColor="white"
/>
{condense && (
<Text>
Page {page} of {lastPage}
</Text>
)}
<IconButton
variant="outline"
size="sm"
onClick={nextPage}
isDisabled={page === lastPage}
aria-label="Next page"
icon={<Icon as={FiChevronRight} boxSize={5} strokeWidth={1.5} />}
bgColor="white"
/>
<IconButton
variant="outline"
size="sm"
onClick={goToLastPage}
isDisabled={page === lastPage}
aria-label="Go to last page"
icon={<Icon as={FiChevronsRight} boxSize={5} strokeWidth={1.5} />}
bgColor="white"
/>
</HStack>
</HStack>
);
};
export default Paginator;

View File

@@ -1,26 +0,0 @@
import { VStack, HStack, type StackProps, Text, Divider } from "@chakra-ui/react";
import Link, { type LinkProps } from "next/link";
const StatsCard = ({
title,
href,
children,
...rest
}: { title: string; href: string } & StackProps & LinkProps) => {
return (
<VStack flex={1} borderWidth={1} padding={4} borderRadius={4} borderColor="gray.300" {...rest}>
<HStack w="full" justifyContent="space-between">
<Text fontSize="md" fontWeight="bold">
{title}
</Text>
<Link href={href}>
<Text color="blue">View all</Text>
</Link>
</HStack>
<Divider />
{children}
</VStack>
);
};
export default StatsCard;

View File

@@ -1,46 +0,0 @@
import { Card, CardHeader, Heading, Table, Tbody, HStack, Button, Text } from "@chakra-ui/react";
import { useState } from "react";
import Link from "next/link";
import { useLoggedCalls } from "~/utils/hooks";
import { TableHeader, TableRow } from "../requestLogs/TableRow";
export default function LoggedCallsTable() {
const [expandedRow, setExpandedRow] = useState<string | null>(null);
const { data: loggedCalls } = useLoggedCalls();
return (
<Card width="100%" overflow="hidden">
<CardHeader>
<HStack justifyContent="space-between">
<Heading as="h3" size="sm">
Request Logs
</Heading>
<Button as={Link} href="/request-logs" variant="ghost" colorScheme="blue">
<Text>View All</Text>
</Button>
</HStack>
</CardHeader>
<Table>
<TableHeader />
<Tbody>
{loggedCalls?.calls.map((loggedCall) => {
return (
<TableRow
key={loggedCall.id}
loggedCall={loggedCall}
isExpanded={loggedCall.id === expandedRow}
onToggle={() => {
if (loggedCall.id === expandedRow) {
setExpandedRow(null);
} else {
setExpandedRow(loggedCall.id);
}
}}
/>
);
})}
</Tbody>
</Table>
</Card>
);
}

View File

@@ -1,61 +0,0 @@
import {
ResponsiveContainer,
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
} from "recharts";
import { useMemo } from "react";
import { useSelectedProject } from "~/utils/hooks";
import dayjs from "~/utils/dayjs";
import { api } from "~/utils/api";
export default function UsageGraph() {
const { data: selectedProject } = useSelectedProject();
const stats = api.dashboard.stats.useQuery(
{ projectId: selectedProject?.id ?? "" },
{ enabled: !!selectedProject },
);
const data = useMemo(() => {
return (
stats.data?.periods.map(({ period, numQueries, cost }) => ({
period,
Requests: numQueries,
"Total Spent (USD)": parseFloat(cost.toString()),
})) || []
);
}, [stats.data]);
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={data} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
<XAxis dataKey="period" tickFormatter={(str: string) => dayjs(str).format("MMM D")} />
<YAxis yAxisId="left" dataKey="Requests" orientation="left" stroke="#8884d8" />
<YAxis
yAxisId="right"
dataKey="Total Spent (USD)"
orientation="right"
unit="$"
stroke="#82ca9d"
/>
<Tooltip />
<Legend />
<CartesianGrid stroke="#f5f5f5" />
<Line dataKey="Requests" stroke="#8884d8" yAxisId="left" dot={false} strokeWidth={2} />
<Line
dataKey="Total Spent (USD)"
stroke="#82ca9d"
yAxisId="right"
dot={false}
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
);
}

View File

@@ -1,112 +0,0 @@
import {
HStack,
Icon,
VStack,
Text,
Divider,
Spinner,
AspectRatio,
SkeletonText,
} from "@chakra-ui/react";
import { RiDatabase2Line } 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";
import { useAppStore } from "~/state/store";
type DatasetData = {
name: string;
numEntries: number;
id: string;
createdAt: Date;
updatedAt: Date;
};
export const DatasetCard = ({ dataset }: { dataset: DatasetData }) => {
return (
<AspectRatio ratio={1.2} w="full">
<VStack
as={Link}
href={{ pathname: "/data/[id]", query: { id: dataset.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={RiDatabase2Line} boxSize={4} />
<Text fontWeight="bold">{dataset.name}</Text>
</HStack>
<HStack h="full" spacing={4} flex={1} align="center">
<CountLabel label="Rows" count={dataset.numEntries} />
</HStack>
<HStack w="full" color="gray.500" fontSize="xs" textAlign="center">
<Text flex={1}>Created {formatTimePast(dataset.createdAt)}</Text>
<Divider h={4} orientation="vertical" />
<Text flex={1}>Updated {formatTimePast(dataset.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 NewDatasetCard = () => {
const router = useRouter();
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
const createMutation = api.datasets.create.useMutation();
const [createDataset, isLoading] = useHandledAsyncCallback(async () => {
const newDataset = await createMutation.mutateAsync({ projectId: selectedProjectId ?? "" });
await router.push({ pathname: "/data/[id]", query: { id: newDataset.id } });
}, [createMutation, router, selectedProjectId]);
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={createDataset}
>
<Icon as={isLoading ? Spinner : BsPlusSquare} boxSize={8} />
<Text display={{ base: "none", md: "block" }} ml={2}>
New Dataset
</Text>
</VStack>
</AspectRatio>
);
};
export const DatasetCardSkeleton = () => (
<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>
);

View File

@@ -1,16 +0,0 @@
import { type StackProps } from "@chakra-ui/react";
import { useDatasetEntries } from "~/utils/hooks";
import Paginator from "../Paginator";
const DatasetEntriesPaginator = (props: StackProps) => {
const { data } = useDatasetEntries();
if (!data) return null;
const { count } = data;
return <Paginator count={count} {...props} />;
};
export default DatasetEntriesPaginator;

View File

@@ -1,31 +0,0 @@
import { type StackProps, VStack, Table, Th, Tr, Thead, Tbody, Text } from "@chakra-ui/react";
import { useDatasetEntries } from "~/utils/hooks";
import TableRow from "./TableRow";
import DatasetEntriesPaginator from "./DatasetEntriesPaginator";
const DatasetEntriesTable = (props: StackProps) => {
const { data } = useDatasetEntries();
return (
<VStack justifyContent="space-between" {...props}>
<Table variant="simple" sx={{ "table-layout": "fixed", width: "full" }}>
<Thead>
<Tr>
<Th>Input</Th>
<Th>Output</Th>
</Tr>
</Thead>
<Tbody>{data?.entries.map((entry) => <TableRow key={entry.id} entry={entry} />)}</Tbody>
</Table>
{(!data || data.entries.length) === 0 ? (
<Text alignSelf="flex-start" pl={6} color="gray.500">
No entries found
</Text>
) : (
<DatasetEntriesPaginator />
)}
</VStack>
);
};
export default DatasetEntriesTable;

View File

@@ -1,26 +0,0 @@
import { Button, HStack, useDisclosure } from "@chakra-ui/react";
import { BiImport } from "react-icons/bi";
import { BsStars } from "react-icons/bs";
import { GenerateDataModal } from "./GenerateDataModal";
export const DatasetHeaderButtons = () => {
const generateModalDisclosure = useDisclosure();
return (
<>
<HStack>
<Button leftIcon={<BiImport />} colorScheme="blue" variant="ghost">
Import Data
</Button>
<Button leftIcon={<BsStars />} colorScheme="blue" onClick={generateModalDisclosure.onOpen}>
Generate Data
</Button>
</HStack>
<GenerateDataModal
isOpen={generateModalDisclosure.isOpen}
onClose={generateModalDisclosure.onClose}
/>
</>
);
};

View File

@@ -1,128 +0,0 @@
import {
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
ModalFooter,
Text,
HStack,
VStack,
Icon,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Button,
} from "@chakra-ui/react";
import { BsStars } from "react-icons/bs";
import { useState } from "react";
import { useDataset, useHandledAsyncCallback } from "~/utils/hooks";
import { api } from "~/utils/api";
import AutoResizeTextArea from "~/components/AutoResizeTextArea";
export const GenerateDataModal = ({
isOpen,
onClose,
}: {
isOpen: boolean;
onClose: () => void;
}) => {
const utils = api.useContext();
const datasetId = useDataset().data?.id;
const [numToGenerate, setNumToGenerate] = useState<number>(20);
const [inputDescription, setInputDescription] = useState<string>(
"Each input should contain an email body. Half of the emails should contain event details, and the other half should not.",
);
const [outputDescription, setOutputDescription] = useState<string>(
`Each output should contain "true" or "false", where "true" indicates that the email contains event details.`,
);
const generateEntriesMutation = api.datasetEntries.autogenerateEntries.useMutation();
const [generateEntries, generateEntriesInProgress] = useHandledAsyncCallback(async () => {
if (!inputDescription || !outputDescription || !numToGenerate || !datasetId) return;
await generateEntriesMutation.mutateAsync({
datasetId,
inputDescription,
outputDescription,
numToGenerate,
});
await utils.datasetEntries.list.invalidate();
onClose();
}, [
generateEntriesMutation,
onClose,
inputDescription,
outputDescription,
numToGenerate,
datasetId,
]);
return (
<Modal isOpen={isOpen} onClose={onClose} size={{ base: "xl", sm: "2xl", md: "3xl" }}>
<ModalOverlay />
<ModalContent w={1200}>
<ModalHeader>
<HStack>
<Icon as={BsStars} />
<Text>Generate Data</Text>
</HStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody maxW="unset">
<VStack w="full" spacing={8} padding={8} alignItems="flex-start">
<VStack alignItems="flex-start" spacing={2}>
<Text fontWeight="bold">Number of Rows:</Text>
<NumberInput
step={5}
defaultValue={15}
min={0}
max={100}
onChange={(valueString) => setNumToGenerate(parseInt(valueString) || 0)}
value={numToGenerate}
w="24"
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</VStack>
<VStack alignItems="flex-start" w="full" spacing={2}>
<Text fontWeight="bold">Input Description:</Text>
<AutoResizeTextArea
value={inputDescription}
onChange={(e) => setInputDescription(e.target.value)}
placeholder="Each input should contain..."
/>
</VStack>
<VStack alignItems="flex-start" w="full" spacing={2}>
<Text fontWeight="bold">Output Description (optional):</Text>
<AutoResizeTextArea
value={outputDescription}
onChange={(e) => setOutputDescription(e.target.value)}
placeholder="The output should contain..."
/>
</VStack>
</VStack>
</ModalBody>
<ModalFooter>
<Button
colorScheme="blue"
isLoading={generateEntriesInProgress}
isDisabled={!numToGenerate || !inputDescription || !outputDescription}
onClick={generateEntries}
>
Generate
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};

View File

@@ -1,13 +0,0 @@
import { Td, Tr } from "@chakra-ui/react";
import { type DatasetEntry } from "@prisma/client";
const TableRow = ({ entry }: { entry: DatasetEntry }) => {
return (
<Tr key={entry.id}>
<Td>{entry.input}</Td>
<Td>{entry.output}</Td>
</Tr>
);
};
export default TableRow;

View File

@@ -1,188 +0,0 @@
import { useState, useEffect, useRef } from "react";
import {
Heading,
VStack,
Icon,
HStack,
Image,
Text,
Box,
Link as ChakraLink,
Flex,
useBreakpointValue,
} from "@chakra-ui/react";
import Head from "next/head";
import Link from "next/link";
import { BsGearFill, BsGithub, BsPersonCircle } from "react-icons/bs";
import { IoStatsChartOutline } from "react-icons/io5";
import { RiHome3Line, RiDatabase2Line, RiFlaskLine } from "react-icons/ri";
import { signIn, useSession } from "next-auth/react";
import { env } from "~/env.mjs";
import ProjectMenu from "./ProjectMenu";
import NavSidebarOption from "./NavSidebarOption";
import IconLink from "./IconLink";
const Divider = () => <Box h="1px" bgColor="gray.300" w="full" />;
const NavSidebar = () => {
const user = useSession().data;
// Hack to get around initial flash, see https://github.com/chakra-ui/chakra-ui/issues/6452
const isMobile = useBreakpointValue({ base: true, md: false, ssr: false });
const renderCount = useRef(0);
renderCount.current++;
const displayLogo = isMobile && renderCount.current > 1;
return (
<VStack
align="stretch"
py={2}
px={2}
pb={0}
height="100%"
w={{ base: "56px", md: "240px" }}
overflow="hidden"
borderRightWidth={1}
borderColor="gray.300"
>
{displayLogo && (
<>
<HStack
as={Link}
href="/"
_hover={{ textDecoration: "none" }}
spacing={{ base: 1, md: 0 }}
mx={2}
py={{ base: 1, md: 2 }}
>
<Image src="/logo.svg" alt="" boxSize={6} mr={4} ml={{ base: 0.5, md: 0 }} />
<Heading size="md" fontFamily="inconsolata, monospace">
OpenPipe
</Heading>
</HStack>
<Divider />
</>
)}
<VStack align="flex-start" overflowY="auto" overflowX="hidden" flex={1}>
{user != null && (
<>
<ProjectMenu />
<Divider />
{env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS && (
<>
<IconLink icon={RiHome3Line} label="Dashboard" href="/dashboard" beta />
<IconLink
icon={IoStatsChartOutline}
label="Request Logs"
href="/request-logs"
beta
/>
</>
)}
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
{env.NEXT_PUBLIC_SHOW_DATA && (
<IconLink icon={RiDatabase2Line} label="Data" href="/data" />
)}
<VStack w="full" alignItems="flex-start" spacing={0} pt={8}>
<Text
pl={2}
pb={2}
fontSize="xs"
fontWeight="bold"
color="gray.500"
display={{ base: "none", md: "flex" }}
>
CONFIGURATION
</Text>
<IconLink icon={BsGearFill} label="Project Settings" href="/project/settings" />
</VStack>
</>
)}
{user === null && (
<NavSidebarOption>
<HStack
w="full"
p={4}
as={ChakraLink}
justifyContent="start"
onClick={() => {
signIn("github").catch(console.error);
}}
>
<Icon as={BsPersonCircle} boxSize={6} mr={2} />
<Text fontWeight="bold" fontSize="sm">
Sign In
</Text>
</HStack>
</NavSidebarOption>
)}
</VStack>
<Divider />
<VStack spacing={0} align="center">
<ChakraLink
href="https://github.com/openpipe/openpipe"
target="_blank"
color="gray.500"
_hover={{ color: "gray.800" }}
p={2}
>
<Icon as={BsGithub} boxSize={6} />
</ChakraLink>
</VStack>
</VStack>
);
};
export default function AppShell({
children,
title,
requireAuth,
}: {
children: React.ReactNode;
title?: string;
requireAuth?: boolean;
}) {
const [vh, setVh] = useState("100vh"); // Default height to prevent flicker on initial render
useEffect(() => {
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);
};
}, []);
const user = useSession().data;
const authLoading = useSession().status === "loading";
useEffect(() => {
if (requireAuth && user === null && !authLoading) {
signIn("github").catch(console.error);
}
}, [requireAuth, user, authLoading]);
return (
<Flex h={vh} w="100vw">
<Head>
<title>{title ? `${title} | OpenPipe` : "OpenPipe"}</title>
</Head>
<NavSidebar />
<Box h="100%" flex={1} overflowY="auto" bgColor="gray.50">
{children}
</Box>
</Flex>
);
}

View File

@@ -1,31 +0,0 @@
import { Icon, HStack, Text, type BoxProps } from "@chakra-ui/react";
import Link, { type LinkProps } from "next/link";
import { type IconType } from "react-icons";
import NavSidebarOption from "./NavSidebarOption";
type IconLinkProps = BoxProps &
LinkProps & { label?: string; icon: IconType; href: string; beta?: boolean };
const IconLink = ({ icon, label, href, color, beta, ...props }: IconLinkProps) => {
return (
<Link href={href} style={{ width: "100%" }}>
<NavSidebarOption activeHrefPattern={href}>
<HStack w="full" justifyContent="space-between" p={2} color={color} {...props}>
<HStack w="full" justifyContent="start">
<Icon as={icon} boxSize={6} mr={2} />
<Text fontSize="sm" display={{ base: "none", md: "block" }}>
{label}
</Text>
</HStack>
{beta && (
<Text fontSize="xs" ml={2} fontWeight="bold" color="orange.400">
BETA
</Text>
)}
</HStack>
</NavSidebarOption>
</Link>
);
};
export default IconLink;

View File

@@ -1,29 +0,0 @@
import { Box, type BoxProps, forwardRef } from "@chakra-ui/react";
import { useRouter } from "next/router";
const NavSidebarOption = forwardRef<
{ activeHrefPattern?: string; disableHoverEffect?: boolean } & BoxProps,
"div"
>(({ activeHrefPattern, disableHoverEffect, ...props }, ref) => {
const router = useRouter();
const isActive = activeHrefPattern && router.pathname.startsWith(activeHrefPattern);
return (
<Box
w="full"
fontWeight={isActive ? "bold" : "500"}
bgColor={isActive ? "gray.200" : "transparent"}
_hover={disableHoverEffect ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
justifyContent="start"
cursor="pointer"
borderRadius={4}
{...props}
ref={ref}
>
{props.children}
</Box>
);
});
NavSidebarOption.displayName = "NavSidebarOption";
export default NavSidebarOption;

View File

@@ -1,19 +0,0 @@
import { Flex, type FlexProps } from "@chakra-ui/react";
const PageHeaderContainer = (props: FlexProps) => {
return (
<Flex
px={8}
py={2}
minH={16}
w="full"
direction={{ base: "column", sm: "row" }}
alignItems={{ base: "flex-start", sm: "center" }}
justifyContent="space-between"
fontWeight="500"
{...props}
/>
);
};
export default PageHeaderContainer;

View File

@@ -1,28 +0,0 @@
import { HStack, Flex, Text } from "@chakra-ui/react";
import { useSelectedProject } from "~/utils/hooks";
// Have to export only contents here instead of full BreadcrumbItem because Chakra doesn't
// recognize a BreadcrumbItem exported with this component as a valid child of Breadcrumb.
export default function ProjectBreadcrumbContents({ projectName = "" }: { projectName?: string }) {
const { data: selectedProject } = useSelectedProject();
projectName = projectName || selectedProject?.name || "";
return (
<HStack w="full">
<Flex
p={1}
borderRadius={4}
backgroundColor="orange.100"
boxSize={6}
alignItems="center"
justifyContent="center"
>
<Text>{projectName[0]?.toUpperCase()}</Text>
</Flex>
<Text display={{ base: "none", md: "block" }} py={1}>
{projectName}
</Text>
</HStack>
);
}

View File

@@ -1,201 +0,0 @@
import {
HStack,
VStack,
Text,
Popover,
PopoverTrigger,
PopoverContent,
Flex,
Icon,
Divider,
Button,
useDisclosure,
Spinner,
Link as ChakraLink,
Image,
Box,
} from "@chakra-ui/react";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { BsPlus, BsPersonCircle } from "react-icons/bs";
import { type Project } from "@prisma/client";
import { useAppStore } from "~/state/store";
import { api } from "~/utils/api";
import NavSidebarOption from "./NavSidebarOption";
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
import { useRouter } from "next/router";
import { useSession, signOut } from "next-auth/react";
export default function ProjectMenu() {
const router = useRouter();
const utils = api.useContext();
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
const { data: projects } = api.projects.list.useQuery();
useEffect(() => {
if (
projects &&
projects[0] &&
(!selectedProjectId || !projects.find((proj) => proj.id === selectedProjectId))
) {
setSelectedProjectId(projects[0].id);
}
}, [selectedProjectId, setSelectedProjectId, projects]);
const { data: selectedProject } = useSelectedProject();
const popover = useDisclosure();
const createMutation = api.projects.create.useMutation();
const [createProject, isLoading] = useHandledAsyncCallback(async () => {
const newProj = await createMutation.mutateAsync({ name: "Untitled Project" });
await utils.projects.list.invalidate();
setSelectedProjectId(newProj.id);
await router.push({ pathname: "/project/settings" });
}, [createMutation, router]);
const user = useSession().data;
const profileImage = user?.user.image ? (
<Image src={user.user.image} alt="profile picture" boxSize={6} borderRadius="50%" />
) : (
<Icon as={BsPersonCircle} boxSize={6} />
);
return (
<VStack w="full" alignItems="flex-start" spacing={0} py={1}>
<Popover
placement="bottom"
isOpen={popover.isOpen}
onOpen={popover.onOpen}
onClose={popover.onClose}
closeOnBlur
>
<PopoverTrigger>
<NavSidebarOption>
<HStack w="full">
<Flex
p={1}
borderRadius={4}
backgroundColor="orange.100"
minW={{ base: 10, md: 8 }}
minH={{ base: 10, md: 8 }}
m={{ base: 0, md: 1 }}
alignItems="center"
justifyContent="center"
>
<Text>{selectedProject?.name[0]?.toUpperCase()}</Text>
</Flex>
<Text
fontSize="sm"
display={{ base: "none", md: "block" }}
py={1}
flex={1}
fontWeight="bold"
>
{selectedProject?.name}
</Text>
<Box mr={2}>{profileImage}</Box>
</HStack>
</NavSidebarOption>
</PopoverTrigger>
<PopoverContent
_focusVisible={{ outline: "unset" }}
ml={-1}
w={224}
boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);"
fontSize="sm"
>
<VStack alignItems="flex-start" spacing={1} py={1}>
<Text px={3} py={2}>
{user?.user.email}
</Text>
<Divider />
<Text alignSelf="flex-start" fontWeight="bold" px={3} pt={2}>
Your Projects
</Text>
<VStack spacing={0} w="full" px={1}>
{projects?.map((proj) => (
<ProjectOption
key={proj.id}
proj={proj}
isActive={proj.id === selectedProjectId}
onClose={popover.onClose}
/>
))}
<HStack
as={Button}
variant="ghost"
colorScheme="blue"
color="blue.400"
fontSize="sm"
justifyContent="flex-start"
onClick={createProject}
w="full"
borderRadius={4}
spacing={0}
>
<Text>Add project</Text>
<Icon as={isLoading ? Spinner : BsPlus} boxSize={4} strokeWidth={0.5} />
</HStack>
</VStack>
<Divider />
<VStack w="full" px={1}>
<ChakraLink
onClick={() => {
signOut().catch(console.error);
}}
_hover={{ bgColor: "gray.200", textDecoration: "none" }}
w="full"
py={2}
px={2}
borderRadius={4}
>
<Text>Sign out</Text>
</ChakraLink>
</VStack>
</VStack>
</PopoverContent>
</Popover>
</VStack>
);
}
const ProjectOption = ({
proj,
isActive,
onClose,
}: {
proj: Project;
isActive: boolean;
onClose: () => void;
}) => {
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
const [gearHovered, setGearHovered] = useState(false);
return (
<HStack
as={Link}
href="/experiments"
onClick={() => {
setSelectedProjectId(proj.id);
onClose();
}}
w="full"
justifyContent="space-between"
_hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
color={isActive ? "blue.400" : undefined}
py={2}
px={4}
borderRadius={4}
spacing={4}
>
<Text>{proj.name}</Text>
</HStack>
);
};

View File

@@ -1,89 +0,0 @@
import {
Button,
AlertDialog,
AlertDialogBody,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogContent,
AlertDialogOverlay,
Input,
Text,
VStack,
Box,
Spinner,
} from "@chakra-ui/react";
import { useRouter } from "next/router";
import { useRef, useState } from "react";
import { api } from "~/utils/api";
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
export const DeleteProjectDialog = ({
isOpen,
onClose,
}: {
isOpen: boolean;
onClose: () => void;
}) => {
const selectedProject = useSelectedProject();
const deleteMutation = api.projects.delete.useMutation();
const utils = api.useContext();
const router = useRouter();
const cancelRef = useRef<HTMLButtonElement>(null);
const [onDeleteConfirm, isDeleting] = useHandledAsyncCallback(async () => {
if (!selectedProject.data?.id) return;
await deleteMutation.mutateAsync({ id: selectedProject.data.id });
await utils.projects.list.invalidate();
await router.push({ pathname: "/experiments" });
onClose();
}, [deleteMutation, selectedProject, router]);
const [nameToDelete, setNameToDelete] = useState("");
return (
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
Delete Project
</AlertDialogHeader>
<AlertDialogBody>
<VStack spacing={4} alignItems="flex-start">
<Text>
If you delete this project all the associated data and experiments will be deleted
as well. If you are sure that you want to delete this project, please type the name
of the project below.
</Text>
<Box bgColor="orange.100" w="full" p={2} borderRadius={4}>
<Text fontFamily="inconsolata">{selectedProject.data?.name}</Text>
</Box>
<Input
placeholder={selectedProject.data?.name}
value={nameToDelete}
onChange={(e) => setNameToDelete(e.target.value)}
/>
</VStack>
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose}>
Cancel
</Button>
<Button
colorScheme="red"
onClick={onDeleteConfirm}
ml={3}
isDisabled={nameToDelete !== selectedProject.data?.name}
w={20}
>
{isDeleting ? <Spinner /> : "Delete"}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
);
};

View File

@@ -1,30 +0,0 @@
import { Button, HStack, type ButtonProps, Icon, Text } from "@chakra-ui/react";
import { type IconType } from "react-icons";
const ActionButton = ({
icon,
label,
...buttonProps
}: { icon: IconType; label: string } & ButtonProps) => {
return (
<Button
colorScheme="blue"
color="black"
bgColor="white"
borderColor="gray.300"
borderRadius={4}
variant="outline"
size="sm"
fontSize="sm"
fontWeight="normal"
{...buttonProps}
>
<HStack spacing={1}>
{icon && <Icon as={icon} />}
<Text>{label}</Text>
</HStack>
</Button>
);
};
export default ActionButton;

View File

@@ -1,55 +0,0 @@
import { Box, IconButton, useToast } from "@chakra-ui/react";
import { CopyIcon } from "lucide-react";
import SyntaxHighlighter from "react-syntax-highlighter";
import { atelierCaveLight } from "react-syntax-highlighter/dist/cjs/styles/hljs";
import stringify from "json-stringify-pretty-compact";
const FormattedJson = ({ json }: { json: any }) => {
const jsonString = stringify(json, { maxLength: 40 });
const toast = useToast();
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast({
title: "Copied to clipboard",
status: "success",
duration: 2000,
});
} catch (err) {
toast({
title: "Failed to copy to clipboard",
status: "error",
duration: 2000,
});
}
};
return (
<Box position="relative" fontSize="sm" borderRadius="md" overflow="hidden">
<SyntaxHighlighter
customStyle={{ overflowX: "unset" }}
language="json"
style={atelierCaveLight}
lineProps={{
style: { wordBreak: "break-all", whiteSpace: "pre-wrap" },
}}
wrapLines
>
{jsonString}
</SyntaxHighlighter>
<IconButton
aria-label="Copy"
icon={<CopyIcon />}
position="absolute"
top={1}
right={1}
size="xs"
variant="ghost"
onClick={() => void copyToClipboard(jsonString)}
/>
</Box>
);
};
export { FormattedJson };

View File

@@ -1,16 +0,0 @@
import { type StackProps } from "@chakra-ui/react";
import { useLoggedCalls } from "~/utils/hooks";
import Paginator from "../Paginator";
const LoggedCallsPaginator = (props: StackProps) => {
const { data } = useLoggedCalls();
if (!data) return null;
const { count } = data;
return <Paginator count={count} {...props} />;
};
export default LoggedCallsPaginator;

View File

@@ -1,36 +0,0 @@
import { Card, Table, Tbody } from "@chakra-ui/react";
import { useState } from "react";
import { useLoggedCalls } from "~/utils/hooks";
import { TableHeader, TableRow } from "./TableRow";
export default function LoggedCallsTable() {
const [expandedRow, setExpandedRow] = useState<string | null>(null);
const { data: loggedCalls } = useLoggedCalls();
return (
<Card width="100%" overflow="hidden">
<Table>
<TableHeader showCheckbox />
<Tbody>
{loggedCalls?.calls.map((loggedCall) => {
return (
<TableRow
key={loggedCall.id}
loggedCall={loggedCall}
isExpanded={loggedCall.id === expandedRow}
onToggle={() => {
if (loggedCall.id === expandedRow) {
setExpandedRow(null);
} else {
setExpandedRow(loggedCall.id);
}
}}
showCheckbox
/>
);
})}
</Tbody>
</Table>
</Card>
);
}

View File

@@ -1,164 +0,0 @@
import {
Box,
Heading,
Td,
Tr,
Thead,
Th,
Tooltip,
Collapse,
HStack,
VStack,
Button,
ButtonGroup,
Text,
Checkbox,
} from "@chakra-ui/react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import Link from "next/link";
import { type RouterOutputs } from "~/utils/api";
import { FormattedJson } from "./FormattedJson";
import { useAppStore } from "~/state/store";
import { useLoggedCalls } from "~/utils/hooks";
import { useMemo } from "react";
dayjs.extend(relativeTime);
type LoggedCall = RouterOutputs["loggedCalls"]["list"]["calls"][0];
export const TableHeader = ({ showCheckbox }: { showCheckbox?: boolean }) => {
const matchingLogIds = useLoggedCalls().data?.matchingLogIds;
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
const addAll = useAppStore((s) => s.selectedLogs.addSelectedLogIds);
const clearAll = useAppStore((s) => s.selectedLogs.clearSelectedLogIds);
const allSelected = useMemo(() => {
if (!matchingLogIds) return false;
return matchingLogIds.every((id) => selectedLogIds.has(id));
}, [selectedLogIds, matchingLogIds]);
return (
<Thead>
<Tr>
{showCheckbox && (
<Th>
<HStack w={8}>
<Checkbox
isChecked={allSelected}
onChange={() => {
allSelected ? clearAll() : addAll(matchingLogIds || []);
}}
/>
<Text>({selectedLogIds.size})</Text>
</HStack>
</Th>
)}
<Th>Time</Th>
<Th>Model</Th>
<Th isNumeric>Duration</Th>
<Th isNumeric>Input tokens</Th>
<Th isNumeric>Output tokens</Th>
<Th isNumeric>Status</Th>
</Tr>
</Thead>
);
};
export const TableRow = ({
loggedCall,
isExpanded,
onToggle,
showCheckbox,
}: {
loggedCall: LoggedCall;
isExpanded: boolean;
onToggle: () => void;
showCheckbox?: boolean;
}) => {
const isError = loggedCall.modelResponse?.statusCode !== 200;
const timeAgo = dayjs(loggedCall.requestedAt).fromNow();
const fullTime = dayjs(loggedCall.requestedAt).toString();
const durationCell = (
<Td isNumeric>
{loggedCall.cacheHit ? (
<Text color="gray.500">Cached</Text>
) : (
((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2) + "s"
)}
</Td>
);
const isChecked = useAppStore((s) => s.selectedLogs.selectedLogIds.has(loggedCall.id));
const toggleChecked = useAppStore((s) => s.selectedLogs.toggleSelectedLogId);
return (
<>
<Tr
onClick={onToggle}
key={loggedCall.id}
_hover={{ bgColor: "gray.50", cursor: "pointer" }}
sx={{
"> td": { borderBottom: "none" },
}}
>
{showCheckbox && (
<Td>
<Checkbox isChecked={isChecked} onChange={() => toggleChecked(loggedCall.id)} />
</Td>
)}
<Td>
<Tooltip label={fullTime} placement="top">
<Box whiteSpace="nowrap" minW="120px">
{timeAgo}
</Box>
</Tooltip>
</Td>
<Td width="100%">
<HStack justifyContent="flex-start">
<Text
colorScheme="purple"
color="purple.500"
borderColor="purple.500"
px={1}
borderRadius={4}
borderWidth={1}
fontSize="xs"
>
{loggedCall.model}
</Text>
</HStack>
</Td>
{durationCell}
<Td isNumeric>{loggedCall.modelResponse?.inputTokens}</Td>
<Td isNumeric>{loggedCall.modelResponse?.outputTokens}</Td>
<Td sx={{ color: isError ? "red.500" : "green.500", fontWeight: "semibold" }} isNumeric>
{loggedCall.modelResponse?.statusCode ?? "No response"}
</Td>
</Tr>
<Tr>
<Td colSpan={8} p={0}>
<Collapse in={isExpanded} unmountOnExit={true}>
<VStack p={4} align="stretch">
<HStack align="stretch">
<VStack flex={1} align="stretch">
<Heading size="sm">Input</Heading>
<FormattedJson json={loggedCall.modelResponse?.reqPayload} />
</VStack>
<VStack flex={1} align="stretch">
<Heading size="sm">Output</Heading>
<FormattedJson json={loggedCall.modelResponse?.respPayload} />
</VStack>
</HStack>
<ButtonGroup alignSelf="flex-end">
<Button as={Link} colorScheme="blue" href={{ pathname: "/experiments" }}>
Experiments
</Button>
</ButtonGroup>
</VStack>
</Collapse>
</Td>
</Tr>
</>
);
};

View File

@@ -1,64 +0,0 @@
{
"type": "object",
"properties": {
"model": {
"description": "The model that will complete your prompt.",
"x-oaiTypeLabel": "string",
"type": "string",
"enum": [
"claude-2",
"claude-2.0",
"claude-instant-1",
"claude-instant-1.1",
"claude-instant-1.2"
]
},
"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: all instructions for the assistant\\n\\nAssistant:\". The prompt string should begin with the characters \"Human:\" and end with \"Assistant:\".",
"default": "<|endoftext|>",
"example": "\\n\\nHuman: What is the correct translation of ${scenario.input}? I would like a long analysis followed by a short answer.\\n\\nAssistant:",
"type": "string"
},
"max_tokens_to_sample": {
"type": "integer",
"minimum": 1,
"default": 256,
"nullable": true,
"description": "The maximum number of tokens to generate before stopping."
},
"temperature": {
"type": "number",
"minimum": 0,
"maximum": 1,
"nullable": true,
"description": "Amount of randomness injected into the response.\n\nDefaults to 1."
},
"top_p": {
"type": "number",
"minimum": 0,
"maximum": 1,
"nullable": true,
"description": "Use nucleus sampling.\n\nYou should either alter temperature or top_p, but not both.\n"
},
"top_k": {
"type": "number",
"minimum": 0,
"default": 5,
"nullable": true,
"description": "Only sample from the top K options for each subsequent token."
},
"stream": {
"description": "Whether to incrementally stream the response using server-sent events.",
"type": "boolean",
"nullable": true,
"default": false
},
"stop_sequences": {
"description": "Sequences that will cause the model to stop generating completion text.\nBy default, our models stop on \"\\n\\nHuman:\".",
"default": null,
"nullable": true,
"type": "array"
}
},
"required": ["model", "prompt", "max_tokens_to_sample"]
}

View File

@@ -1,51 +0,0 @@
import { type Session } from "next-auth";
import { SessionProvider } from "next-auth/react";
import { type AppType } from "next/app";
import { api } from "~/utils/api";
import Favicon from "~/components/Favicon";
import Head from "next/head";
import { ChakraThemeProvider } from "~/theme/ChakraThemeProvider";
import { SyncAppStore } from "~/state/sync";
import NextAdapterApp from "next-query-params/app";
import { QueryParamProvider } from "use-query-params";
import { PosthogAppProvider } from "~/utils/analytics/posthog";
const MyApp: AppType<{ session: Session | null }> = ({
Component,
pageProps: { session, ...pageProps },
}) => {
return (
<>
<Head>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
/>
<meta name="og:title" content="OpenPipe: Open-Source Lab for LLMs" key="title" />
<meta
name="og:description"
content="OpenPipe is a powerful playground for quickly optimizing performance, cost, and speed across models."
key="description"
/>
<meta name="og:image" content="/og.png" key="og-image" />
<meta property="og:image:height" content="630" />
<meta property="og:image:width" content="1200" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="/og.png" />
</Head>
<SessionProvider session={session}>
<PosthogAppProvider>
<SyncAppStore />
<Favicon />
<ChakraThemeProvider>
<QueryParamProvider adapter={NextAdapterApp}>
<Component {...pageProps} />
</QueryParamProvider>
</ChakraThemeProvider>
</PosthogAppProvider>
</SessionProvider>
</>
);
};
export default api.withTRPC(MyApp);

View File

@@ -1,22 +0,0 @@
import { type NextApiRequest, type NextApiResponse } from "next";
import cors from "nextjs-cors";
import { createOpenApiNextHandler } from "trpc-openapi";
import { createProcedureCache } from "trpc-openapi/dist/adapters/node-http/procedures";
import { appRouter } from "~/server/api/root.router";
import { createTRPCContext } from "~/server/api/trpc";
const openApiHandler = createOpenApiNextHandler({
router: appRouter,
createContext: createTRPCContext,
});
const cache = createProcedureCache(appRouter);
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Setup CORS
await cors(req, res);
return openApiHandler(req, res);
};
export default handler;

View File

@@ -1,81 +0,0 @@
import { ImageResponse } from "@vercel/og";
import { type NextApiRequest, type NextApiResponse } from "next";
export const config = {
runtime: "experimental-edge",
};
const inconsolataRegularFontP = fetch(
new URL("../../../../public/fonts/Inconsolata_SemiExpanded-Medium.ttf", import.meta.url),
).then((res) => res.arrayBuffer());
const OgImage = async (req: NextApiRequest, _res: NextApiResponse) => {
// @ts-expect-error - nextUrl is not defined on NextApiRequest for some reason
const searchParams = req.nextUrl?.searchParams as URLSearchParams;
const experimentLabel = searchParams.get("experimentLabel");
const variantsCount = searchParams.get("variantsCount");
const scenariosCount = searchParams.get("scenariosCount");
const inconsolataRegularFont = await inconsolataRegularFontP;
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
fontSize: 48,
padding: "48px",
background: "white",
position: "relative",
}}
>
<div
style={{
position: "absolute",
top: 0,
left: 0,
display: "flex",
alignItems: "center",
padding: 48,
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="https://app.openpipe.ai/logo.svg"
alt="OpenPipe Logo"
height={100}
width={120}
/>
<div style={{ marginLeft: 24, fontSize: 64, fontFamily: "Inconsolata" }}>OpenPipe</div>
</div>
<div style={{ display: "flex", fontSize: 72, marginTop: 108 }}>{experimentLabel}</div>
<div style={{ display: "flex", flexDirection: "column", marginTop: 36 }}>
<div style={{ display: "flex" }}>
<span style={{ width: 320 }}>Variants:</span> {variantsCount}
</div>
<div style={{ display: "flex", marginTop: 24 }}>
<span style={{ width: 320 }}>Scenarios:</span> {scenariosCount}
</div>
</div>
</div>
),
{
fonts: [
{
name: "inconsolata",
data: inconsolataRegularFont,
style: "normal",
weight: 400,
},
],
},
);
};
export default OgImage;

View File

@@ -1,16 +0,0 @@
import { type NextApiRequest, type NextApiResponse } from "next";
import { generateOpenApiDocument } from "trpc-openapi";
import { appRouter } from "~/server/api/root.router";
export const openApiDocument = generateOpenApiDocument(appRouter, {
title: "OpenPipe API",
description: "The public API for reporting API calls to OpenPipe",
version: "0.1.0",
baseUrl: "https://app.openpipe.ai/api",
});
// Respond with our OpenAPI schema
const hander = (req: NextApiRequest, res: NextApiResponse) => {
res.status(200).send(openApiDocument);
};
export default hander;

View File

@@ -1,6 +0,0 @@
// A faulty API route to test Sentry's error monitoring
// @ts-expect-error just a test file, don't care about types
export default function handler(_req, res) {
throw new Error("Sentry Example API Route Error");
res.status(200).json({ name: "John Doe" });
}

View File

@@ -1,114 +0,0 @@
import {
Heading,
Text,
Stat,
StatLabel,
StatNumber,
VStack,
HStack,
Card,
CardBody,
CardHeader,
Icon,
Table,
Tbody,
Tr,
Td,
Divider,
} from "@chakra-ui/react";
import { Ban, DollarSign, Hash } from "lucide-react";
import AppShell from "~/components/nav/AppShell";
import { useSelectedProject } from "~/utils/hooks";
import { api } from "~/utils/api";
import LoggedCallsTable from "~/components/dashboard/LoggedCallsTable";
import UsageGraph from "~/components/dashboard/UsageGraph";
export default function Dashboard() {
const { data: selectedProject } = useSelectedProject();
const stats = api.dashboard.stats.useQuery(
{ projectId: selectedProject?.id ?? "" },
{ enabled: !!selectedProject },
);
return (
<AppShell title="Dashboard" requireAuth>
<VStack px={8} py={8} alignItems="flex-start" spacing={4}>
<Text fontSize="2xl" fontWeight="bold">
Dashboard
</Text>
<Divider />
<VStack margin="auto" spacing={4} align="stretch" w="full">
<HStack gap={4} align="start">
<Card flex={1}>
<CardHeader>
<Heading as="h3" size="sm">
Usage Statistics
</Heading>
</CardHeader>
<CardBody>
<UsageGraph />
</CardBody>
</Card>
<VStack spacing="4" width="300px" align="stretch">
<Card>
<CardBody>
<Stat>
<HStack>
<StatLabel flex={1}>Total Spent</StatLabel>
<Icon as={DollarSign} boxSize={4} color="gray.500" />
</HStack>
<StatNumber>
${parseFloat(stats.data?.totals?.cost?.toString() ?? "0").toFixed(3)}
</StatNumber>
</Stat>
</CardBody>
</Card>
<Card>
<CardBody>
<Stat>
<HStack>
<StatLabel flex={1}>Total Requests</StatLabel>
<Icon as={Hash} boxSize={4} color="gray.500" />
</HStack>
<StatNumber>
{stats.data?.totals?.numQueries
? parseInt(stats.data?.totals?.numQueries.toString())?.toLocaleString()
: undefined}
</StatNumber>
</Stat>
</CardBody>
</Card>
<Card overflow="hidden">
<Stat>
<CardHeader>
<HStack>
<StatLabel flex={1}>Errors</StatLabel>
<Icon as={Ban} boxSize={4} color="gray.500" />
</HStack>
</CardHeader>
<Table variant="simple">
<Tbody>
{stats.data?.errors?.map((error) => (
<Tr key={error.code}>
<Td>
{error.name} ({error.code})
</Td>
<Td isNumeric color="red.600">
{parseInt(error.count.toString()).toLocaleString()}
</Td>
</Tr>
))}
</Tbody>
</Table>
</Stat>
</Card>
</VStack>
</HStack>
<LoggedCallsTable />
</VStack>
</VStack>
</AppShell>
);
}

View File

@@ -1,97 +0,0 @@
import {
Box,
Breadcrumb,
BreadcrumbItem,
Center,
Flex,
Icon,
Input,
VStack,
} from "@chakra-ui/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState, useEffect } from "react";
import { RiDatabase2Line } from "react-icons/ri";
import AppShell from "~/components/nav/AppShell";
import { api } from "~/utils/api";
import { useDataset, useHandledAsyncCallback } from "~/utils/hooks";
import DatasetEntriesTable from "~/components/datasets/DatasetEntriesTable";
import { DatasetHeaderButtons } from "~/components/datasets/DatasetHeaderButtons/DatasetHeaderButtons";
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
export default function Dataset() {
const router = useRouter();
const utils = api.useContext();
const dataset = useDataset();
const datasetId = router.query.id as string;
const [name, setName] = useState(dataset.data?.name || "");
useEffect(() => {
setName(dataset.data?.name || "");
}, [dataset.data?.name]);
const updateMutation = api.datasets.update.useMutation();
const [onSaveName] = useHandledAsyncCallback(async () => {
if (name && name !== dataset.data?.name && dataset.data?.id) {
await updateMutation.mutateAsync({
id: dataset.data.id,
updates: { name: name },
});
await Promise.all([utils.datasets.list.invalidate(), utils.datasets.get.invalidate()]);
}
}, [updateMutation, dataset.data?.id, dataset.data?.name, name]);
if (!dataset.isLoading && !dataset.data) {
return (
<AppShell title="Dataset not found">
<Center h="100%">
<div>Dataset not found 😕</div>
</Center>
</AppShell>
);
}
return (
<AppShell title={dataset.data?.name}>
<VStack h="full">
<PageHeaderContainer>
<Breadcrumb>
<BreadcrumbItem>
<ProjectBreadcrumbContents projectName={dataset.data?.project?.name} />
</BreadcrumbItem>
<BreadcrumbItem>
<Link href="/data">
<Flex alignItems="center" _hover={{ textDecoration: "underline" }}>
<Icon as={RiDatabase2Line} boxSize={4} mr={2} /> Datasets
</Flex>
</Link>
</BreadcrumbItem>
<BreadcrumbItem isCurrentPage>
<Input
size="sm"
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={onSaveName}
borderWidth={1}
borderColor="transparent"
fontSize={16}
px={0}
minW={{ base: 100, lg: 300 }}
flex={1}
_hover={{ borderColor: "gray.300" }}
_focus={{ borderColor: "blue.500", outline: "none" }}
/>
</BreadcrumbItem>
</Breadcrumb>
<DatasetHeaderButtons />
</PageHeaderContainer>
<Box w="full" overflowX="auto" flex={1} px={8} pt={8} pb={16}>
{datasetId && <DatasetEntriesTable />}
</Box>
</VStack>
</AppShell>
);
}

View File

@@ -1,49 +0,0 @@
import { SimpleGrid, Icon, Breadcrumb, BreadcrumbItem, Flex } from "@chakra-ui/react";
import AppShell from "~/components/nav/AppShell";
import { RiDatabase2Line } from "react-icons/ri";
import {
DatasetCard,
DatasetCardSkeleton,
NewDatasetCard,
} from "~/components/datasets/DatasetCard";
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
import { useDatasets } from "~/utils/hooks";
export default function DatasetsPage() {
const datasets = useDatasets();
return (
<AppShell title="Data" requireAuth>
<PageHeaderContainer>
<Breadcrumb>
<BreadcrumbItem>
<ProjectBreadcrumbContents />
</BreadcrumbItem>
<BreadcrumbItem minH={8}>
<Flex alignItems="center">
<Icon as={RiDatabase2Line} boxSize={4} mr={2} /> Datasets
</Flex>
</BreadcrumbItem>
</Breadcrumb>
</PageHeaderContainer>
<SimpleGrid w="full" columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing={8} py={4} px={8}>
<NewDatasetCard />
{datasets.data && !datasets.isLoading ? (
datasets?.data?.map((dataset) => (
<DatasetCard
key={dataset.id}
dataset={{ ...dataset, numEntries: dataset._count.datasetEntries }}
/>
))
) : (
<>
<DatasetCardSkeleton />
<DatasetCardSkeleton />
<DatasetCardSkeleton />
</>
)}
</SimpleGrid>
</AppShell>
);
}

View File

@@ -1,154 +0,0 @@
import {
Box,
Breadcrumb,
BreadcrumbItem,
Center,
Flex,
Icon,
Input,
Text,
VStack,
} from "@chakra-ui/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState, useEffect } from "react";
import { RiFlaskLine } from "react-icons/ri";
import OutputsTable from "~/components/OutputsTable";
import ExperimentSettingsDrawer from "~/components/ExperimentSettingsDrawer/ExperimentSettingsDrawer";
import AppShell from "~/components/nav/AppShell";
import { api } from "~/utils/api";
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
import { useAppStore } from "~/state/store";
import { useSyncVariantEditor } from "~/state/sync";
import { ExperimentHeaderButtons } from "~/components/experiments/ExperimentHeaderButtons/ExperimentHeaderButtons";
import Head from "next/head";
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
// TODO: import less to fix deployment with server side props
// export const getServerSideProps = async (context: GetServerSidePropsContext<{ id: string }>) => {
// const experimentId = context.params?.id as string;
// const helpers = createServerSideHelpers({
// router: appRouter,
// ctx: createInnerTRPCContext({ session: null }),
// transformer: superjson, // optional - adds superjson serialization
// });
// // prefetch query
// await helpers.experiments.stats.prefetch({ id: experimentId });
// return {
// props: {
// trpcState: helpers.dehydrate(),
// },
// };
// };
export default function Experiment() {
const router = useRouter();
const utils = api.useContext();
useSyncVariantEditor();
const experiment = useExperiment();
const experimentStats = api.experiments.stats.useQuery(
{ id: router.query.id as string },
{
enabled: !!router.query.id,
},
);
const stats = experimentStats.data;
useEffect(() => {
useAppStore.getState().sharedVariantEditor.loadMonaco().catch(console.error);
}, []);
const [label, setLabel] = useState(experiment.data?.label || "");
useEffect(() => {
setLabel(experiment.data?.label || "");
}, [experiment.data?.label]);
const updateMutation = api.experiments.update.useMutation();
const [onSaveLabel] = useHandledAsyncCallback(async () => {
if (label && label !== experiment.data?.label && experiment.data?.id) {
await updateMutation.mutateAsync({
id: experiment.data.id,
updates: { label: label },
});
await Promise.all([utils.experiments.list.invalidate(), utils.experiments.get.invalidate()]);
}
}, [updateMutation, experiment.data?.id, experiment.data?.label, label]);
if (!experiment.isLoading && !experiment.data) {
return (
<AppShell title="Experiment not found">
<Center h="100%">
<div>Experiment not found 😕</div>
</Center>
</AppShell>
);
}
const canModify = experiment.data?.access.canModify ?? false;
return (
<>
{stats && (
<Head>
<meta property="og:title" content={stats.experimentLabel} key="title" />
<meta
property="og:image"
content={`/api/experiments/og-image?experimentLabel=${stats.experimentLabel}&variantsCount=${stats.promptVariantCount}&scenariosCount=${stats.testScenarioCount}`}
key="og-image"
/>
</Head>
)}
<AppShell title={experiment.data?.label}>
<VStack h="full">
<PageHeaderContainer>
<Breadcrumb>
<BreadcrumbItem>
<ProjectBreadcrumbContents projectName={experiment.data?.project?.name} />
</BreadcrumbItem>
<BreadcrumbItem>
<Link href="/experiments">
<Flex alignItems="center" _hover={{ textDecoration: "underline" }}>
<Icon as={RiFlaskLine} boxSize={4} mr={2} /> Experiments
</Flex>
</Link>
</BreadcrumbItem>
<BreadcrumbItem isCurrentPage>
{canModify ? (
<Input
size="sm"
value={label}
onChange={(e) => setLabel(e.target.value)}
onBlur={onSaveLabel}
borderWidth={1}
borderColor="transparent"
fontSize={16}
px={0}
minW={{ base: 100, lg: 300 }}
flex={1}
_hover={{ borderColor: "gray.300" }}
_focus={{ borderColor: "blue.500", outline: "none" }}
/>
) : (
<Text fontSize={16} px={0} minW={{ base: 100, lg: 300 }} flex={1}>
{experiment.data?.label}
</Text>
)}
</BreadcrumbItem>
</Breadcrumb>
<ExperimentHeaderButtons />
</PageHeaderContainer>
<ExperimentSettingsDrawer />
<Box w="100%" overflowX="auto" flex={1}>
<OutputsTable experimentId={router.query.id as string | undefined} />
</Box>
</VStack>
</AppShell>
</>
);
}

View File

@@ -1,44 +0,0 @@
import { SimpleGrid, Icon, Breadcrumb, BreadcrumbItem, Flex } from "@chakra-ui/react";
import { RiFlaskLine } from "react-icons/ri";
import AppShell from "~/components/nav/AppShell";
import {
ExperimentCard,
ExperimentCardSkeleton,
NewExperimentCard,
} from "~/components/experiments/ExperimentCard";
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
import { useExperiments } from "~/utils/hooks";
export default function ExperimentsPage() {
const experiments = useExperiments();
return (
<AppShell title="Experiments" requireAuth>
<PageHeaderContainer>
<Breadcrumb>
<BreadcrumbItem>
<ProjectBreadcrumbContents />
</BreadcrumbItem>
<BreadcrumbItem minH={8}>
<Flex alignItems="center">
<Icon as={RiFlaskLine} boxSize={4} mr={2} /> Experiments
</Flex>
</BreadcrumbItem>
</Breadcrumb>
</PageHeaderContainer>
<SimpleGrid w="full" columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing={8} py="4" px={8}>
<NewExperimentCard />
{experiments.data && !experiments.isLoading ? (
experiments?.data?.map((exp) => <ExperimentCard key={exp.id} exp={exp} />)
) : (
<>
<ExperimentCardSkeleton />
<ExperimentCardSkeleton />
<ExperimentCardSkeleton />
</>
)}
</SimpleGrid>
</AppShell>
);
}

View File

@@ -1,161 +0,0 @@
import {
Breadcrumb,
BreadcrumbItem,
Text,
type TextProps,
VStack,
HStack,
Button,
Divider,
Icon,
useDisclosure,
} from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { BsTrash } from "react-icons/bs";
import AppShell from "~/components/nav/AppShell";
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
import { api } from "~/utils/api";
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
import CopiableCode from "~/components/CopiableCode";
import { DeleteProjectDialog } from "~/components/projectSettings/DeleteProjectDialog";
import AutoResizeTextArea from "~/components/AutoResizeTextArea";
export default function Settings() {
const utils = api.useContext();
const { data: selectedProject } = useSelectedProject();
const apiKey =
selectedProject?.apiKeys?.length && selectedProject?.apiKeys[0]
? selectedProject?.apiKeys[0].apiKey
: "";
const updateMutation = api.projects.update.useMutation();
const [onSaveName] = useHandledAsyncCallback(async () => {
if (name && name !== selectedProject?.name && selectedProject?.id) {
await updateMutation.mutateAsync({
id: selectedProject.id,
updates: { name },
});
await Promise.all([
utils.projects.get.invalidate({ id: selectedProject.id }),
utils.projects.list.invalidate(),
]);
}
}, [updateMutation, selectedProject]);
const [name, setName] = useState(selectedProject?.name);
useEffect(() => {
setName(selectedProject?.name);
}, [selectedProject?.name]);
const deleteProjectOpen = useDisclosure();
return (
<>
<AppShell>
<PageHeaderContainer>
<Breadcrumb>
<BreadcrumbItem>
<ProjectBreadcrumbContents />
</BreadcrumbItem>
<BreadcrumbItem isCurrentPage>
<Text>Project Settings</Text>
</BreadcrumbItem>
</Breadcrumb>
</PageHeaderContainer>
<VStack px={8} py={4} alignItems="flex-start" spacing={4}>
<VStack spacing={0} alignItems="flex-start">
<Text fontSize="2xl" fontWeight="bold">
Project Settings
</Text>
<Text fontSize="sm">
Configure your project settings. These settings only apply to {selectedProject?.name}.
</Text>
</VStack>
<VStack
w="full"
alignItems="flex-start"
borderWidth={1}
borderRadius={4}
borderColor="gray.300"
bgColor="white"
p={6}
spacing={6}
>
<VStack alignItems="flex-start" w="full">
<Text fontWeight="bold" fontSize="xl">
Display Name
</Text>
<AutoResizeTextArea
w="full"
maxW={600}
value={name}
onChange={(e) => setName(e.target.value)}
borderColor="gray.300"
/>
<Button
isDisabled={!name || name === selectedProject?.name}
colorScheme="orange"
borderRadius={4}
mt={2}
_disabled={{
opacity: 0.6,
}}
onClick={onSaveName}
>
Rename Project
</Button>
</VStack>
<Divider backgroundColor="gray.300" />
<VStack alignItems="flex-start">
<Subtitle>Project API Key</Subtitle>
<Text fontSize="sm">
Use your project API key to authenticate your requests when sending data to
OpenPipe. You can set this key in your environment variables, or use it directly in
your code.
</Text>
</VStack>
<CopiableCode code={apiKey} />
<Divider />
{selectedProject?.personalProjectUserId ? (
<VStack alignItems="flex-start">
<Subtitle>Personal Project</Subtitle>
<Text fontSize="sm">
This project is {selectedProject?.personalProjectUser?.name}'s personal project.
It cannot be deleted.
</Text>
</VStack>
) : (
<VStack alignItems="flex-start">
<Subtitle color="red.600">Danger Zone</Subtitle>
<Text fontSize="sm">
Permanently delete your project and all of its data. This action cannot be undone.
</Text>
<HStack
as={Button}
isDisabled={selectedProject?.role !== "ADMIN"}
colorScheme="red"
variant="outline"
borderRadius={4}
mt={2}
height="auto"
onClick={deleteProjectOpen.onOpen}
>
<Icon as={BsTrash} />
<Text overflowWrap="break-word" whiteSpace="normal" py={2}>
Delete {selectedProject?.name}
</Text>
</HStack>
</VStack>
)}
</VStack>
</VStack>
</AppShell>
<DeleteProjectDialog isOpen={deleteProjectOpen.isOpen} onClose={deleteProjectOpen.onClose} />
</>
);
}
const Subtitle = (props: TextProps) => <Text fontWeight="bold" fontSize="xl" {...props} />;

View File

@@ -1,34 +0,0 @@
import { Text, VStack, Divider, HStack } from "@chakra-ui/react";
import AppShell from "~/components/nav/AppShell";
import LoggedCallTable from "~/components/requestLogs/LoggedCallsTable";
import LoggedCallsPaginator from "~/components/requestLogs/LoggedCallsPaginator";
import ActionButton from "~/components/requestLogs/ActionButton";
import { useAppStore } from "~/state/store";
import { RiFlaskLine } from "react-icons/ri";
export default function LoggedCalls() {
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
return (
<AppShell title="Request Logs" requireAuth>
<VStack px={8} py={8} alignItems="flex-start" spacing={4} w="full">
<Text fontSize="2xl" fontWeight="bold">
Request Logs
</Text>
<Divider />
<HStack w="full" justifyContent="flex-end">
<ActionButton
onClick={() => {
console.log("experimenting with these ids", selectedLogIds);
}}
label="Experiment"
icon={RiFlaskLine}
isDisabled={selectedLogIds.size === 0}
/>
</HStack>
<LoggedCallTable />
<LoggedCallsPaginator />
</VStack>
</AppShell>
);
}

View File

@@ -1,84 +0,0 @@
import Head from "next/head";
import * as Sentry from "@sentry/nextjs";
export default function Home() {
return (
<div>
<Head>
<title>Sentry Onboarding</title>
<meta name="description" content="Test Sentry for your Next.js app!" />
</Head>
<main
style={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<h1 style={{ fontSize: "4rem", margin: "14px 0" }}>
<svg
style={{
height: "1em",
}}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 200 44"
>
<path
fill="currentColor"
d="M124.32,28.28,109.56,9.22h-3.68V34.77h3.73V15.19l15.18,19.58h3.26V9.22h-3.73ZM87.15,23.54h13.23V20.22H87.14V12.53h14.93V9.21H83.34V34.77h18.92V31.45H87.14ZM71.59,20.3h0C66.44,19.06,65,18.08,65,15.7c0-2.14,1.89-3.59,4.71-3.59a12.06,12.06,0,0,1,7.07,2.55l2-2.83a14.1,14.1,0,0,0-9-3c-5.06,0-8.59,3-8.59,7.27,0,4.6,3,6.19,8.46,7.52C74.51,24.74,76,25.78,76,28.11s-2,3.77-5.09,3.77a12.34,12.34,0,0,1-8.3-3.26l-2.25,2.69a15.94,15.94,0,0,0,10.42,3.85c5.48,0,9-2.95,9-7.51C79.75,23.79,77.47,21.72,71.59,20.3ZM195.7,9.22l-7.69,12-7.64-12h-4.46L186,24.67V34.78h3.84V24.55L200,9.22Zm-64.63,3.46h8.37v22.1h3.84V12.68h8.37V9.22H131.08ZM169.41,24.8c3.86-1.07,6-3.77,6-7.63,0-4.91-3.59-8-9.38-8H154.67V34.76h3.8V25.58h6.45l6.48,9.2h4.44l-7-9.82Zm-10.95-2.5V12.6h7.17c3.74,0,5.88,1.77,5.88,4.84s-2.29,4.86-5.84,4.86Z M29,2.26a4.67,4.67,0,0,0-8,0L14.42,13.53A32.21,32.21,0,0,1,32.17,40.19H27.55A27.68,27.68,0,0,0,12.09,17.47L6,28a15.92,15.92,0,0,1,9.23,12.17H4.62A.76.76,0,0,1,4,39.06l2.94-5a10.74,10.74,0,0,0-3.36-1.9l-2.91,5a4.54,4.54,0,0,0,1.69,6.24A4.66,4.66,0,0,0,4.62,44H19.15a19.4,19.4,0,0,0-8-17.31l2.31-4A23.87,23.87,0,0,1,23.76,44H36.07a35.88,35.88,0,0,0-16.41-31.8l4.67-8a.77.77,0,0,1,1.05-.27c.53.29,20.29,34.77,20.66,35.17a.76.76,0,0,1-.68,1.13H40.6q.09,1.91,0,3.81h4.78A4.59,4.59,0,0,0,50,39.43a4.49,4.49,0,0,0-.62-2.28Z"
></path>
</svg>
</h1>
<p>Get started by sending us a sample error:</p>
<button
type="button"
style={{
padding: "12px",
cursor: "pointer",
backgroundColor: "#AD6CAA",
borderRadius: "4px",
border: "none",
color: "white",
fontSize: "14px",
margin: "18px",
}}
onClick={async () => {
const transaction = Sentry.startTransaction({
name: "Example Frontend Transaction",
});
Sentry.configureScope((scope) => {
scope.setSpan(transaction);
});
try {
const res = await fetch("/api/sentry-example-api");
if (!res.ok) {
throw new Error("Sentry Example Frontend Error");
}
} finally {
transaction.finish();
}
}}
>
Throw error!
</button>
<p>
Next, look for the error on the{" "}
<a href="https://openpipe.sentry.io/issues/?project=4505642011394048">Issues Page</a>.
</p>
<p style={{ marginTop: "24px" }}>
For more information, see{" "}
<a href="https://docs.sentry.io/platforms/javascript/guides/nextjs/">
https://docs.sentry.io/platforms/javascript/guides/nextjs/
</a>
</p>
</main>
</div>
);
}

View File

@@ -1,15 +0,0 @@
import { type GetServerSideProps } from "next";
// eslint-disable-next-line @typescript-eslint/require-await
export const getServerSideProps: GetServerSideProps = async () => {
return {
redirect: {
destination: "/world-champs/signup",
permanent: false,
},
};
};
export default function WorldChamps() {
return null;
}

View File

@@ -1,273 +0,0 @@
import {
Box,
type BoxProps,
Button,
DarkMode,
GlobalStyle,
HStack,
Heading,
Icon,
Link,
Table,
Tbody,
Td,
Text,
type TextProps,
Th,
Tr,
VStack,
useInterval,
Image,
Flex,
Alert,
AlertIcon,
} from "@chakra-ui/react";
import { signIn, useSession } from "next-auth/react";
import Head from "next/head";
import { useCallback, useState } from "react";
import { BsGithub } from "react-icons/bs";
import UserMenu from "~/components/nav/UserMenu";
import { api } from "~/utils/api";
import dayjs from "~/utils/dayjs";
import { useHandledAsyncCallback } from "~/utils/hooks";
import GitHubButton from "react-github-btn";
const TopNavbar = () => (
<HStack px={4} py={2} align="center" justify="center">
<HStack
as={Link}
href="/"
_hover={{ textDecoration: "none" }}
spacing={0}
py={2}
pr={16}
flex={1}
sx={{
".widget": {
display: "block",
},
}}
>
<Image src="/logo.svg" alt="" boxSize={6} mr={4} />
<Heading size="md" fontFamily="inconsolata, monospace">
OpenPipe
</Heading>
</HStack>
<Box pt="6px">
<GitHubButton
href="https://github.com/openpipe/openpipe"
data-color-scheme="no-preference: dark; light: dark; dark: dark;"
data-size="large"
aria-label="Follow @openpipe on GitHub"
>
Github
</GitHubButton>
</Box>
</HStack>
);
// Shows how long until the competition starts. Refreshes every second
function CountdownTimer(props: { date: Date } & TextProps) {
const [now, setNow] = useState(dayjs());
useInterval(() => {
setNow(dayjs());
}, 1000);
const { date, ...rest } = props;
const kickoff = dayjs(date);
const diff = kickoff.diff(now, "second");
const days = Math.floor(diff / 86400);
const hours = Math.floor((diff % 86400) / 3600);
const minutes = Math.floor((diff % 3600) / 60);
const seconds = Math.floor(diff % 60);
return (
<Text {...rest} suppressHydrationWarning>
<Text as="span" fontWeight="bold">
Kickoff in
</Text>{" "}
{days}d {hours}h {minutes}m {seconds}s
</Text>
);
}
function ApplicationStatus(props: BoxProps) {
const user = useSession().data;
const entrant = api.worldChamps.userStatus.useQuery().data;
const applyMutation = api.worldChamps.apply.useMutation();
const utils = api.useContext();
const [onSignIn] = useHandledAsyncCallback(async () => {
await signIn("github");
}, []);
const [onApply] = useHandledAsyncCallback(async () => {
await applyMutation.mutateAsync();
await utils.worldChamps.userStatus.invalidate();
}, []);
const Wrapper = useCallback(
(wrapperProps: BoxProps) => (
<Box {...props} {...wrapperProps} minH="120px" alignItems="center" justifyItems="center" />
),
[props],
);
if (user === null) {
return (
<Wrapper>
<Button onClick={onSignIn} colorScheme="orange" leftIcon={<Icon as={BsGithub} />}>
Connect GitHub to apply
</Button>
</Wrapper>
);
} else if (user) {
return (
<Wrapper>
<Flex flexDirection={{ base: "column", md: "row" }} alignItems="center">
<UserMenu
user={user}
borderRadius={2}
borderColor={"gray.700"}
borderWidth={1}
pr={6}
mr={{ base: 0, md: 8 }}
mb={{ base: 8, md: 0 }}
/>
<Box flex={1}>
{entrant?.approved ? (
<Text fontSize="sm">
You're accepted! We'll send you more details before August 14th.
</Text>
) : entrant ? (
<Text fontSize="sm">
Application submitted successfully. We'll notify you by email before August 14th.{" "}
<Link
href="https://github.com/openpipe/openpipe"
isExternal
textDecor="underline"
fontWeight="bold"
>
Star our Github ⭐
</Link>{" "}
for updates while you wait!
</Text>
) : (
<Button onClick={onApply} colorScheme="orange">
Apply to compete
</Button>
)}
</Box>
</Flex>
</Wrapper>
);
}
return <Wrapper />;
}
export default function Signup() {
return (
<DarkMode>
<GlobalStyle />
<Head>
<title>🏆 Prompt Engineering World Championships</title>
<meta property="og:title" content="🏆 Prompt Engineering World Championships" key="title" />
<meta
property="og:description"
content="Think you have what it takes to be the best? Compete with the world's top prompt engineers and see where you rank!"
key="description"
/>
</Head>
<Box color="gray.200" minH="100vh" w="full">
<TopNavbar />
<VStack mx="auto" py={24} maxW="2xl" px={4} align="center" fontSize="lg">
<Heading size="lg" textAlign="center">
🏆 Prompt Engineering World Championships
</Heading>
{/* <CountdownTimer
date={new Date("2023-08-14T00:00:00Z")}
fontSize="2xl"
alignSelf="center"
color="gray.500"
/> */}
<Alert status="warning" mt={4}>
<AlertIcon />
We've decided to pause the World Championships for the moment because our systems aren't
quite ready. You can still sign up if you're interested and we'll notify you once we
reschedule!
</Alert>
<ApplicationStatus py={8} alignSelf="center" />
<Text fontSize="lg" textAlign="left">
Think you have what it takes to be the best? Compete with the world's top prompt
engineers and see where you rank!
</Text>
<Heading size="lg" pt={12} alignSelf="left">
Event Details
</Heading>
<Table variant="simple">
<Tbody
sx={{
th: {
base: { px: 0 },
md: { px: 6 },
},
}}
>
<Tr>
<Th>Kickoff</Th>
<Td>August 14</Td>
</Tr>
<Tr>
<Th>Prize</Th>
<Td>$15,000 grand prize + smaller category prizes.</Td>
</Tr>
<Tr>
<Th>Events</Th>
<Td>
Optimize prompts for multiple tasks selected from academic benchmarks and
real-world applications.
</Td>
</Tr>
<Tr>
<Th>Models</Th>
<Td>Separate "weight classes" for GPT 3.5, Claude Instant, and Llama 2.</Td>
</Tr>
<Tr>
<Th>Qualifications</Th>
<Td>Open to entrants with any level of experience.</Td>
</Tr>
<Tr>
<Th>Certificates</Th>
<Td>Certificate of mastery for all qualifying participants.</Td>
</Tr>
<Tr>
<Th>Cost</Th>
<Td>
<strong>Free</strong>. We'll cover your inference budget.
</Td>
</Tr>
<Tr>
<Th>Questions?</Th>
<Td>
<Link href="mailto:world-champs@openpipe.ai" textDecor="underline">
Email us
</Link>{" "}
with any follow-up questions!
</Td>
</Tr>
</Tbody>
</Table>
</VStack>
</Box>
</DarkMode>
);
}

View File

@@ -1,125 +0,0 @@
import "dotenv/config";
import * as recast from "recast";
import { type ASTNode } from "ast-types";
import { fileURLToPath } from "url";
import parsePromptConstructor from "./parse";
import { prisma } from "~/server/db";
import { promptConstructorVersion } from "./version";
const { builders: b } = recast.types;
export const migrate1to2 = (fnBody: string): string => {
const ast: ASTNode = recast.parse(fnBody);
recast.visit(ast, {
visitAssignmentExpression(path) {
const node = path.node;
if ("name" in node.left && node.left.name === "prompt") {
const functionCall = b.callExpression(b.identifier("definePrompt"), [
b.literal("openai/ChatCompletion"),
node.right,
]);
path.replace(functionCall);
}
return false;
},
});
return recast.print(ast).code;
};
export const migrate2to3 = (fnBody: string): string => {
const ast: ASTNode = recast.parse(fnBody);
recast.visit(ast, {
visitCallExpression(path) {
const node = path.node;
// Check if the function being called is 'definePrompt'
if (
recast.types.namedTypes.Identifier.check(node.callee) &&
node.callee.name === "definePrompt" &&
node.arguments.length > 0 &&
recast.types.namedTypes.Literal.check(node.arguments[0]) &&
node.arguments[0].value === "anthropic"
) {
node.arguments[0].value = "anthropic/completion";
}
return false;
},
});
return recast.print(ast).code;
};
const migrations: Record<number, (fnBody: string) => string> = {
2: migrate1to2,
3: migrate2to3,
};
const applyMigrations = (
promptConstructor: string,
currentVersion: number,
targetVersion: number,
) => {
let migratedFn = promptConstructor;
for (let v = currentVersion + 1; v <= targetVersion; v++) {
const migrationFn = migrations[v];
if (migrationFn) {
migratedFn = migrationFn(migratedFn);
}
}
return migratedFn;
};
export default async function migrateConstructFns(targetVersion: number) {
const prompts = await prisma.promptVariant.findMany({
where: { promptConstructorVersion: { lt: targetVersion } },
});
console.log(`Migrating ${prompts.length} prompts to version ${targetVersion}`);
await Promise.all(
prompts.map(async (variant) => {
const currentVersion = variant.promptConstructorVersion;
try {
const migratedFn = applyMigrations(
variant.promptConstructor,
currentVersion,
targetVersion,
);
const parsedFn = await parsePromptConstructor(migratedFn);
if ("error" in parsedFn) {
throw new Error(parsedFn.error);
}
await prisma.promptVariant.update({
where: {
id: variant.id,
},
data: {
promptConstructor: migratedFn,
promptConstructorVersion: targetVersion,
modelProvider: parsedFn.modelProvider,
model: parsedFn.model,
},
});
} catch (e) {
console.error("Error migrating promptConstructor for variant", variant.id, e);
}
}),
);
}
// If we're running this file directly, run the migration to the latest version
if (process.argv.at(-1) === fileURLToPath(import.meta.url)) {
const latestVersion = Math.max(...Object.keys(migrations).map(Number));
if (latestVersion !== promptConstructorVersion) {
throw new Error(
`The latest migration is ${latestVersion}, but the promptConstructorVersion is ${promptConstructorVersion}`,
);
}
await migrateConstructFns(promptConstructorVersion);
console.log("Done");
}

View File

@@ -1 +0,0 @@
export const promptConstructorVersion = 3;

View File

@@ -1,108 +0,0 @@
import { type ChatCompletion } from "openai/resources/chat";
import { openai } from "../../utils/openai";
import { isAxiosError } from "./utils";
import { type APIResponse } from "openai/core";
import { sleep } from "~/server/utils/sleep";
const MAX_AUTO_RETRIES = 50;
const MIN_DELAY = 500; // milliseconds
const MAX_DELAY = 15000; // milliseconds
function calculateDelay(numPreviousTries: number): number {
const baseDelay = Math.min(MAX_DELAY, MIN_DELAY * Math.pow(2, numPreviousTries));
const jitter = Math.random() * baseDelay;
return baseDelay + jitter;
}
const getCompletionWithBackoff = async (
getCompletion: () => Promise<APIResponse<ChatCompletion>>,
) => {
let completion;
let tries = 0;
while (tries < MAX_AUTO_RETRIES) {
try {
completion = await getCompletion();
break;
} catch (e) {
if (isAxiosError(e)) {
console.error(e?.response?.data?.error?.message);
} else {
await sleep(calculateDelay(tries));
console.error(e);
}
}
tries++;
}
return completion;
};
// TODO: Add seeds to ensure batches don't contain duplicate data
const MAX_BATCH_SIZE = 5;
export const autogenerateDatasetEntries = async (
numToGenerate: number,
inputDescription: string,
outputDescription: string,
): Promise<{ input: string; output: string }[]> => {
const batchSizes = Array.from({ length: Math.ceil(numToGenerate / MAX_BATCH_SIZE) }, (_, i) =>
i === Math.ceil(numToGenerate / MAX_BATCH_SIZE) - 1 && numToGenerate % MAX_BATCH_SIZE
? numToGenerate % MAX_BATCH_SIZE
: MAX_BATCH_SIZE,
);
const getCompletion = (batchSize: number) =>
openai.chat.completions.create({
model: "gpt-4",
messages: [
{
role: "system",
content: `The user needs ${batchSize} rows of data, each with an input and an output.\n---\n The input should follow these requirements: ${inputDescription}\n---\n The output should follow these requirements: ${outputDescription}`,
},
],
functions: [
{
name: "add_list_of_data",
description: "Add a list of data to the database",
parameters: {
type: "object",
properties: {
rows: {
type: "array",
description: "The rows of data that match the description",
items: {
type: "object",
properties: {
input: {
type: "string",
description: "The input for this row",
},
output: {
type: "string",
description: "The output for this row",
},
},
},
},
},
},
},
],
function_call: { name: "add_list_of_data" },
temperature: 0.5,
});
const completionCallbacks = batchSizes.map((batchSize) =>
getCompletionWithBackoff(() => getCompletion(batchSize)),
);
const completions = await Promise.all(completionCallbacks);
const rows = completions.flatMap((completion) => {
const parsed = JSON.parse(
completion?.choices[0]?.message?.function_call?.arguments ?? "{rows: []}",
) as { rows: { input: string; output: string }[] };
return parsed.rows;
});
return rows;
};

View File

@@ -1,18 +0,0 @@
type AxiosError = {
response?: {
data?: {
error?: {
message?: string;
};
};
};
};
export function isAxiosError(error: unknown): error is AxiosError {
if (typeof error === "object" && error !== null) {
// Initial check
const err = error as AxiosError;
return err.response?.data?.error?.message !== undefined; // Check structure
}
return false;
}

View File

@@ -1,108 +0,0 @@
import { sql } from "kysely";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { kysely } from "~/server/db";
import { requireCanViewProject } from "~/utils/accessControl";
import dayjs from "~/utils/dayjs";
export const dashboardRouter = createTRPCRouter({
stats: protectedProcedure
.input(
z.object({
// TODO: actually take startDate into account
startDate: z.string().optional(),
projectId: z.string(),
}),
)
.query(async ({ input, ctx }) => {
await requireCanViewProject(input.projectId, ctx);
// Return the stats group by hour
const periods = await kysely
.selectFrom("LoggedCall")
.leftJoin(
"LoggedCallModelResponse",
"LoggedCall.id",
"LoggedCallModelResponse.originalLoggedCallId",
)
.where("projectId", "=", input.projectId)
.select(({ fn }) => [
sql<Date>`date_trunc('day', "LoggedCallModelResponse"."requestedAt")`.as("period"),
sql<number>`count("LoggedCall"."id")::int`.as("numQueries"),
fn.sum(fn.coalesce("LoggedCallModelResponse.cost", sql<number>`0`)).as("cost"),
])
.groupBy("period")
.orderBy("period")
.execute();
let originalDataIndex = periods.length - 1;
// *SLAMS DOWN GLASS OF WHISKEY* timezones, amirite?
let dayToMatch = dayjs(input.startDate || new Date());
// Ensure that the initial date we're matching against is never before the first period
if (
periods[originalDataIndex] &&
dayToMatch.isBefore(periods[originalDataIndex]?.period, "day")
) {
dayToMatch = dayjs(periods[originalDataIndex]?.period);
}
const backfilledPeriods: typeof periods = [];
// Backfill from now to 14 days ago or the date of the first logged call, whichever is earlier
while (
backfilledPeriods.length < 14 ||
(periods[0]?.period && !dayToMatch.isBefore(periods[0]?.period, "day"))
) {
const nextOriginalPeriod = periods[originalDataIndex];
if (nextOriginalPeriod && dayjs(nextOriginalPeriod?.period).isSame(dayToMatch, "day")) {
backfilledPeriods.unshift(nextOriginalPeriod);
originalDataIndex--;
} else {
backfilledPeriods.unshift({
period: dayjs(dayToMatch).toDate(),
numQueries: 0,
cost: 0,
});
}
dayToMatch = dayToMatch.subtract(1, "day");
}
const totals = await kysely
.selectFrom("LoggedCall")
.leftJoin(
"LoggedCallModelResponse",
"LoggedCall.id",
"LoggedCallModelResponse.originalLoggedCallId",
)
.where("projectId", "=", input.projectId)
.select(({ fn }) => [
fn.sum(fn.coalesce("LoggedCallModelResponse.cost", sql<number>`0`)).as("cost"),
fn.count("LoggedCall.id").as("numQueries"),
])
.executeTakeFirst();
const errors = await kysely
.selectFrom("LoggedCall")
.where("projectId", "=", input.projectId)
.leftJoin(
"LoggedCallModelResponse",
"LoggedCall.id",
"LoggedCallModelResponse.originalLoggedCallId",
)
.select(({ fn }) => [fn.count("LoggedCall.id").as("count"), "statusCode as code"])
.where("statusCode", ">", 200)
.groupBy("code")
.orderBy("count", "desc")
.execute();
const namedErrors = errors.map((e) => {
if (e.code === 429) {
return { ...e, name: "Rate limited" };
} else if (e.code === 500) {
return { ...e, name: "Internal server error" };
} else {
return { ...e, name: "Other" };
}
});
return { periods: backfilledPeriods, totals, errors: namedErrors };
}),
});

View File

@@ -1,145 +0,0 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { prisma } from "~/server/db";
import { requireCanModifyDataset, requireCanViewDataset } from "~/utils/accessControl";
import { autogenerateDatasetEntries } from "../autogenerate/autogenerateDatasetEntries";
export const datasetEntries = createTRPCRouter({
list: protectedProcedure
.input(z.object({ datasetId: z.string(), page: z.number(), pageSize: z.number() }))
.query(async ({ input, ctx }) => {
await requireCanViewDataset(input.datasetId, ctx);
const { datasetId, page, pageSize } = input;
const entries = await prisma.datasetEntry.findMany({
where: {
datasetId,
},
orderBy: { createdAt: "desc" },
skip: (page - 1) * pageSize,
take: pageSize,
});
const count = await prisma.datasetEntry.count({
where: {
datasetId,
},
});
return {
entries,
count,
};
}),
createOne: protectedProcedure
.input(
z.object({
datasetId: z.string(),
input: z.string(),
output: z.string().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
await requireCanModifyDataset(input.datasetId, ctx);
return await prisma.datasetEntry.create({
data: {
datasetId: input.datasetId,
input: input.input,
output: input.output,
},
});
}),
autogenerateEntries: protectedProcedure
.input(
z.object({
datasetId: z.string(),
numToGenerate: z.number(),
inputDescription: z.string(),
outputDescription: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
await requireCanModifyDataset(input.datasetId, ctx);
const dataset = await prisma.dataset.findUnique({
where: {
id: input.datasetId,
},
});
if (!dataset) {
throw new Error(`Dataset with id ${input.datasetId} does not exist`);
}
const entries = await autogenerateDatasetEntries(
input.numToGenerate,
input.inputDescription,
input.outputDescription,
);
const createdEntries = await prisma.datasetEntry.createMany({
data: entries.map((entry) => ({
datasetId: input.datasetId,
input: entry.input,
output: entry.output,
})),
});
return createdEntries;
}),
delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input, ctx }) => {
const datasetId = (
await prisma.datasetEntry.findUniqueOrThrow({
where: { id: input.id },
})
).datasetId;
await requireCanModifyDataset(datasetId, ctx);
return await prisma.datasetEntry.delete({
where: {
id: input.id,
},
});
}),
update: protectedProcedure
.input(
z.object({
id: z.string(),
updates: z.object({
input: z.string(),
output: z.string().optional(),
}),
}),
)
.mutation(async ({ input, ctx }) => {
const existing = await prisma.datasetEntry.findUnique({
where: {
id: input.id,
},
});
if (!existing) {
throw new Error(`dataEntry with id ${input.id} does not exist`);
}
await requireCanModifyDataset(existing.datasetId, ctx);
return await prisma.datasetEntry.update({
where: {
id: input.id,
},
data: {
input: input.updates.input,
output: input.updates.output,
},
});
}),
});

View File

@@ -1,88 +0,0 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc";
import { prisma } from "~/server/db";
import {
requireCanModifyDataset,
requireCanModifyProject,
requireCanViewDataset,
requireCanViewProject,
} from "~/utils/accessControl";
export const datasetsRouter = createTRPCRouter({
list: protectedProcedure
.input(z.object({ projectId: z.string() }))
.query(async ({ input, ctx }) => {
await requireCanViewProject(input.projectId, ctx);
const datasets = await prisma.dataset.findMany({
where: {
projectId: input.projectId,
},
orderBy: {
createdAt: "desc",
},
include: {
_count: {
select: { datasetEntries: true },
},
},
});
return datasets;
}),
get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
await requireCanViewDataset(input.id, ctx);
return await prisma.dataset.findFirstOrThrow({
where: { id: input.id },
include: {
project: true,
},
});
}),
create: protectedProcedure
.input(z.object({ projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
await requireCanModifyProject(input.projectId, ctx);
const numDatasets = await prisma.dataset.count({
where: {
projectId: input.projectId,
},
});
return await prisma.dataset.create({
data: {
name: `Dataset ${numDatasets + 1}`,
projectId: input.projectId,
},
});
}),
update: protectedProcedure
.input(z.object({ id: z.string(), updates: z.object({ name: z.string() }) }))
.mutation(async ({ input, ctx }) => {
await requireCanModifyDataset(input.id, ctx);
return await prisma.dataset.update({
where: {
id: input.id,
},
data: {
name: input.updates.name,
},
});
}),
delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input, ctx }) => {
await requireCanModifyDataset(input.id, ctx);
await prisma.dataset.delete({
where: {
id: input.id,
},
});
}),
});

Some files were not shown because too many files have changed in this diff Show More