Compare commits

..

5 Commits

Author SHA1 Message Date
David Corbitt
4e176088e9 Auto-resize project menu width 2023-08-10 22:46:01 -07:00
David Corbitt
3cec1f7786 Merge branch 'main' into logged-calls 2023-08-10 22:19:16 -07:00
David Corbitt
b3d8f96fa8 Merge branch 'main' into logged-calls 2023-08-10 21:49:31 -07:00
David Corbitt
54d97ddfa8 Add getUsage function 2023-08-10 19:51:36 -07:00
David Corbitt
1f8e3b820f Rename prompt and completion tokens to input and output tokens 2023-08-10 19:49:18 -07:00
153 changed files with 4132 additions and 6907 deletions

View File

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

5
.gitignore vendored
View File

@@ -1,5 +0,0 @@
.env
.venv/
*.pyc
node_modules/
*.tsbuildinfo

View File

@@ -32,5 +32,5 @@ NEXT_PUBLIC_HOST="http://localhost:3000"
GITHUB_CLIENT_ID="your_client_id" GITHUB_CLIENT_ID="your_client_id"
GITHUB_CLIENT_SECRET="your_secret" GITHUB_CLIENT_SECRET="your_secret"
OPENPIPE_BASE_URL="http://localhost:3000/api/v1" OPENPIPE_BASE_URL="http://localhost:3000/api"
OPENPIPE_API_KEY="your_key" OPENPIPE_API_KEY="your_key"

View File

@@ -6,7 +6,7 @@ const config = {
overrides: [ overrides: [
{ {
extends: ["plugin:@typescript-eslint/recommended-requiring-type-checking"], extends: ["plugin:@typescript-eslint/recommended-requiring-type-checking"],
files: ["*.mts", "*.ts", "*.tsx"], files: ["*.ts", "*.tsx"],
parserOptions: { parserOptions: {
project: path.join(__dirname, "tsconfig.json"), project: path.join(__dirname, "tsconfig.json"),
}, },

3
app/.gitignore vendored
View File

@@ -44,6 +44,3 @@ yarn-error.log*
# Sentry Auth Token # Sentry Auth Token
.sentryclirc .sentryclirc
# custom openai intialization
src/server/utils/openaiCustomConfig.json

View File

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

View File

@@ -6,13 +6,13 @@ RUN yarn global add pnpm
# DEPS # DEPS
FROM base as deps FROM base as deps
WORKDIR /code WORKDIR /app
COPY app/prisma app/package.json ./app/ COPY prisma ./
COPY client-libs/typescript/package.json ./client-libs/typescript/
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
RUN cd app && pnpm install --frozen-lockfile COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# BUILDER # BUILDER
FROM base as builder FROM base as builder
@@ -25,24 +25,22 @@ ARG NEXT_PUBLIC_SENTRY_DSN
ARG SENTRY_AUTH_TOKEN ARG SENTRY_AUTH_TOKEN
ARG NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS ARG NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS
WORKDIR /code WORKDIR /app
COPY --from=deps /code/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /code/app/node_modules ./app/node_modules
COPY --from=deps /code/client-libs/typescript/node_modules ./client-libs/typescript/node_modules
COPY . . COPY . .
RUN cd app && SKIP_ENV_VALIDATION=1 pnpm build RUN SKIP_ENV_VALIDATION=1 pnpm build
# RUNNER # RUNNER
FROM base as runner FROM base as runner
WORKDIR /code/app WORKDIR /app
ENV NODE_ENV production ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1
COPY --from=builder /code/ /code/ COPY --from=builder /app/ ./
EXPOSE 3000 EXPOSE 3000
ENV PORT 3000 ENV PORT 3000
# Run the "run-prod.sh" script # Run the "run-prod.sh" script
CMD /code/app/run-prod.sh CMD /app/run-prod.sh

View File

@@ -36,8 +36,6 @@ let config = {
}); });
return config; return config;
}, },
transpilePackages: ["openpipe"],
}; };
config = nextRoutes()(config); config = nextRoutes()(config);

View File

@@ -1,6 +1,5 @@
{ {
"name": "openpipe-app", "name": "openpipe",
"private": true,
"type": "module", "type": "module",
"version": "0.1.0", "version": "0.1.0",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -17,7 +16,7 @@
"postinstall": "prisma generate", "postinstall": "prisma generate",
"lint": "next lint", "lint": "next lint",
"start": "next start", "start": "next start",
"codegen:clients": "tsx src/server/scripts/client-codegen.ts", "codegen": "tsx src/server/scripts/client-codegen.ts",
"seed": "tsx prisma/seed.ts", "seed": "tsx prisma/seed.ts",
"check": "concurrently 'pnpm lint' 'pnpm tsc' 'pnpm prettier . --check'", "check": "concurrently 'pnpm lint' 'pnpm tsc' 'pnpm prettier . --check'",
"test": "pnpm vitest" "test": "pnpm vitest"
@@ -25,6 +24,7 @@
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.5.8", "@anthropic-ai/sdk": "^0.5.8",
"@apidevtools/json-schema-ref-parser": "^10.1.0", "@apidevtools/json-schema-ref-parser": "^10.1.0",
"@babel/preset-typescript": "^7.22.5",
"@babel/standalone": "^7.22.9", "@babel/standalone": "^7.22.9",
"@chakra-ui/anatomy": "^2.2.0", "@chakra-ui/anatomy": "^2.2.0",
"@chakra-ui/next-js": "^2.1.4", "@chakra-ui/next-js": "^2.1.4",
@@ -72,7 +72,6 @@
"nextjs-cors": "^2.1.2", "nextjs-cors": "^2.1.2",
"nextjs-routes": "^2.0.1", "nextjs-routes": "^2.0.1",
"openai": "4.0.0-beta.7", "openai": "4.0.0-beta.7",
"openpipe": "workspace:*",
"pg": "^8.11.2", "pg": "^8.11.2",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"posthog-js": "^1.75.3", "posthog-js": "^1.75.3",
@@ -129,7 +128,6 @@
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"monaco-editor": "^0.40.0", "monaco-editor": "^0.40.0",
"openapi-typescript": "^6.3.4", "openapi-typescript": "^6.3.4",
"openapi-typescript-codegen": "^0.25.0",
"prisma": "^4.14.0", "prisma": "^4.14.0",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"typescript": "^5.0.4", "typescript": "^5.0.4",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
/*
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.
*/
-- Rename completionTokens to outputTokens
ALTER TABLE "ModelResponse"
RENAME COLUMN "completionTokens" TO "outputTokens";
-- Rename promptTokens to inputTokens
ALTER TABLE "ModelResponse"
RENAME COLUMN "promptTokens" TO "inputTokens";

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

@@ -112,17 +112,17 @@ model ScenarioVariantCell {
model ModelResponse { model ModelResponse {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
cacheKey String inputHash String
requestedAt DateTime? requestedAt DateTime?
receivedAt DateTime? receivedAt DateTime?
respPayload Json? output Json?
cost Float? cost Float?
inputTokens Int? inputTokens Int?
outputTokens Int? outputTokens Int?
statusCode Int? statusCode Int?
errorMessage String? errorMessage String?
retryTime DateTime? retryTime DateTime?
outdated Boolean @default(false) outdated Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -131,7 +131,7 @@ model ModelResponse {
scenarioVariantCell ScenarioVariantCell @relation(fields: [scenarioVariantCellId], references: [id], onDelete: Cascade) scenarioVariantCell ScenarioVariantCell @relation(fields: [scenarioVariantCellId], references: [id], onDelete: Cascade)
outputEvaluations OutputEvaluation[] outputEvaluations OutputEvaluation[]
@@index([cacheKey]) @@index([inputHash])
} }
enum EvalType { enum EvalType {
@@ -256,7 +256,7 @@ model WorldChampEntrant {
model LoggedCall { model LoggedCall {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
requestedAt DateTime startTime DateTime
// True if this call was served from the cache, false otherwise // True if this call was served from the cache, false otherwise
cacheHit Boolean cacheHit Boolean
@@ -273,13 +273,12 @@ model LoggedCall {
projectId String @db.Uuid projectId String @db.Uuid
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade) project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
model String? tags LoggedCallTag[]
tags LoggedCallTag[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@index([requestedAt]) @@index([startTime])
} }
model LoggedCallModelResponse { model LoggedCallModelResponse {
@@ -288,14 +287,14 @@ model LoggedCallModelResponse {
reqPayload Json reqPayload Json
// The HTTP status returned by the model provider // The HTTP status returned by the model provider
statusCode Int? respStatus Int?
respPayload Json? respPayload Json?
// Should be null if the request was successful, and some string if the request failed. // Should be null if the request was successful, and some string if the request failed.
errorMessage String? error String?
requestedAt DateTime startTime DateTime
receivedAt DateTime endTime DateTime
// Note: the function to calculate the cacheKey should include the project // Note: the function to calculate the cacheKey should include the project
// ID so we don't share cached responses between projects, which could be an // ID so we don't share cached responses between projects, which could be an
@@ -309,7 +308,7 @@ model LoggedCallModelResponse {
outputTokens Int? outputTokens Int?
finishReason String? finishReason String?
completionId String? completionId String?
cost Decimal? @db.Decimal(18, 12) totalCost Decimal? @db.Decimal(18, 12)
// The LoggedCall that created this LoggedCallModelResponse // The LoggedCall that created this LoggedCallModelResponse
originalLoggedCallId String @unique @db.Uuid originalLoggedCallId String @unique @db.Uuid
@@ -340,8 +339,8 @@ model ApiKey {
name String name String
apiKey String @unique apiKey String @unique
projectId String @db.Uuid projectId String @db.Uuid
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

@@ -2,7 +2,6 @@ import { prisma } from "~/server/db";
import dedent from "dedent"; import dedent from "dedent";
import { generateNewCell } from "~/server/utils/generateNewCell"; import { generateNewCell } from "~/server/utils/generateNewCell";
import { promptConstructorVersion } from "~/promptConstructor/version"; import { promptConstructorVersion } from "~/promptConstructor/version";
import { env } from "~/env.mjs";
const defaultId = "11111111-1111-1111-1111-111111111111"; const defaultId = "11111111-1111-1111-1111-111111111111";
@@ -17,16 +16,6 @@ const project =
data: { id: defaultId }, data: { id: defaultId },
})); }));
if (env.OPENPIPE_API_KEY) {
await prisma.apiKey.create({
data: {
projectId: project.id,
name: "Default API Key",
apiKey: env.OPENPIPE_API_KEY,
},
});
}
await prisma.experiment.deleteMany({ await prisma.experiment.deleteMany({
where: { where: {
id: defaultId, id: defaultId,

View File

@@ -339,17 +339,17 @@ for (let i = 0; i < 1437; i++) {
MODEL_RESPONSE_TEMPLATES[Math.floor(Math.random() * MODEL_RESPONSE_TEMPLATES.length)]!; MODEL_RESPONSE_TEMPLATES[Math.floor(Math.random() * MODEL_RESPONSE_TEMPLATES.length)]!;
const model = template.reqPayload.model; const model = template.reqPayload.model;
// choose random time in the last two weeks, with a bias towards the last few days // choose random time in the last two weeks, with a bias towards the last few days
const requestedAt = new Date(Date.now() - Math.pow(Math.random(), 2) * 1000 * 60 * 60 * 24 * 14); const startTime = new Date(Date.now() - Math.pow(Math.random(), 2) * 1000 * 60 * 60 * 24 * 14);
// choose random delay anywhere from 2 to 10 seconds later for gpt-4, or 1 to 5 seconds for gpt-3.5 // choose random delay anywhere from 2 to 10 seconds later for gpt-4, or 1 to 5 seconds for gpt-3.5
const delay = const delay =
model === "gpt-4" ? 1000 * 2 + Math.random() * 1000 * 8 : 1000 + Math.random() * 1000 * 4; model === "gpt-4" ? 1000 * 2 + Math.random() * 1000 * 8 : 1000 + Math.random() * 1000 * 4;
const receivedAt = new Date(requestedAt.getTime() + delay); const endTime = new Date(startTime.getTime() + delay);
loggedCallsToCreate.push({ loggedCallsToCreate.push({
id: loggedCallId, id: loggedCallId,
cacheHit: false, cacheHit: false,
requestedAt, startTime,
projectId: project.id, projectId: project.id,
createdAt: requestedAt, createdAt: startTime,
}); });
const { promptTokenPrice, completionTokenPrice } = const { promptTokenPrice, completionTokenPrice } =
@@ -365,20 +365,21 @@ for (let i = 0; i < 1437; i++) {
loggedCallModelResponsesToCreate.push({ loggedCallModelResponsesToCreate.push({
id: loggedCallModelResponseId, id: loggedCallModelResponseId,
requestedAt, startTime,
receivedAt, endTime,
originalLoggedCallId: loggedCallId, originalLoggedCallId: loggedCallId,
reqPayload: template.reqPayload, reqPayload: template.reqPayload,
respPayload: template.respPayload, respPayload: template.respPayload,
statusCode: template.respStatus, respStatus: template.respStatus,
errorMessage: template.error, error: template.error,
createdAt: requestedAt, createdAt: startTime,
cacheKey: hashRequest(project.id, template.reqPayload as JsonValue), cacheKey: hashRequest(project.id, template.reqPayload as JsonValue),
durationMs: receivedAt.getTime() - requestedAt.getTime(), durationMs: endTime.getTime() - startTime.getTime(),
inputTokens: template.inputTokens, inputTokens: template.inputTokens,
outputTokens: template.outputTokens, outputTokens: template.outputTokens,
finishReason: template.finishReason, finishReason: template.finishReason,
cost: template.inputTokens * promptTokenPrice + template.outputTokens * completionTokenPrice, totalCost:
template.inputTokens * promptTokenPrice + template.outputTokens * completionTokenPrice,
}); });
loggedCallsToUpdate.push({ loggedCallsToUpdate.push({
where: { where: {

View File

@@ -33,11 +33,25 @@ export default function AddVariantButton() {
<Flex w="100%" justifyContent="flex-end"> <Flex w="100%" justifyContent="flex-end">
<ActionButton <ActionButton
onClick={onClick} onClick={onClick}
py={7} py={5}
leftIcon={<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />} leftIcon={<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />}
> >
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text> <Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
</ActionButton> </ActionButton>
{/* <Button
alignItems="center"
justifyContent="center"
fontWeight="normal"
bgColor="transparent"
_hover={{ bgColor: "gray.100" }}
px={cellPadding.x}
onClick={onClick}
height="unset"
minH={headerMinHeight}
>
<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
</Button> */}
</Flex> </Flex>
); );
} }

View File

@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import { BsPencil, BsX } from "react-icons/bs"; import { BsPencil, BsX } from "react-icons/bs";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { useExperiment, useHandledAsyncCallback, useScenarioVars } from "~/utils/hooks"; import { useExperiment, useHandledAsyncCallback, useScenarioVars } from "~/utils/hooks";
import { maybeReportError } from "~/utils/errorHandling/maybeReportError"; import { maybeReportError } from "~/utils/standardResponses";
import { FloatingLabelInput } from "./FloatingLabelInput"; import { FloatingLabelInput } from "./FloatingLabelInput";
export const ScenarioVar = ({ export const ScenarioVar = ({

View File

@@ -33,7 +33,7 @@ export default function OutputCell({
if (!templateHasVariables) disabledReason = "Add a value to the scenario variables to see output"; if (!templateHasVariables) disabledReason = "Add a value to the scenario variables to see output";
const [refetchInterval, setRefetchInterval] = useState<number | false>(false); const [refetchInterval, setRefetchInterval] = useState(0);
const { data: cell, isLoading: queryLoading } = api.scenarioVariantCells.get.useQuery( const { data: cell, isLoading: queryLoading } = api.scenarioVariantCells.get.useQuery(
{ scenarioId: scenario.id, variantId: variant.id }, { scenarioId: scenario.id, variantId: variant.id },
{ refetchInterval }, { refetchInterval },
@@ -64,8 +64,7 @@ export default function OutputCell({
cell.retrievalStatus === "PENDING" || cell.retrievalStatus === "PENDING" ||
cell.retrievalStatus === "IN_PROGRESS" || cell.retrievalStatus === "IN_PROGRESS" ||
hardRefetching; hardRefetching;
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : 0), [awaitingOutput]);
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : false), [awaitingOutput]);
// TODO: disconnect from socket if we're not streaming anymore // TODO: disconnect from socket if we're not streaming anymore
const streamedMessage = useSocket<OutputSchema>(cell?.id); const streamedMessage = useSocket<OutputSchema>(cell?.id);
@@ -108,7 +107,7 @@ export default function OutputCell({
if (disabledReason) return <Text color="gray.500">{disabledReason}</Text>; if (disabledReason) return <Text color="gray.500">{disabledReason}</Text>;
const showLogs = !streamedMessage && !mostRecentResponse?.respPayload; const showLogs = !streamedMessage && !mostRecentResponse?.output;
if (showLogs) if (showLogs)
return ( return (
@@ -121,13 +120,8 @@ export default function OutputCell({
? response.receivedAt.getTime() ? response.receivedAt.getTime()
: Date.now(); : Date.now();
if (response.requestedAt) { if (response.requestedAt) {
numWaitingMessages = Math.min( numWaitingMessages = Math.floor(
Math.floor( (relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL,
(relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL,
),
// Don't try to render more than 15, it'll use too much CPU and
// break the page
15,
); );
} }
return ( return (
@@ -166,13 +160,13 @@ export default function OutputCell({
</CellWrapper> </CellWrapper>
); );
const normalizedOutput = mostRecentResponse?.respPayload const normalizedOutput = mostRecentResponse?.output
? provider.normalizeOutput(mostRecentResponse?.respPayload) ? provider.normalizeOutput(mostRecentResponse?.output)
: streamedMessage : streamedMessage
? provider.normalizeOutput(streamedMessage) ? provider.normalizeOutput(streamedMessage)
: null; : null;
if (mostRecentResponse?.respPayload && normalizedOutput?.type === "json") { if (mostRecentResponse?.output && normalizedOutput?.type === "json") {
return ( return (
<CellWrapper> <CellWrapper>
<SyntaxHighlighter <SyntaxHighlighter

View File

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

View File

@@ -10,8 +10,6 @@ const ScenarioRow = (props: {
variants: PromptVariant[]; variants: PromptVariant[];
canHide: boolean; canHide: boolean;
rowStart: number; rowStart: number;
isFirst: boolean;
isLast: boolean;
}) => { }) => {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
@@ -23,14 +21,10 @@ const ScenarioRow = (props: {
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
sx={isHovered ? highlightStyle : undefined} sx={isHovered ? highlightStyle : undefined}
bgColor="white" borderLeftWidth={1}
{...borders}
rowStart={props.rowStart} rowStart={props.rowStart}
colStart={1} colStart={1}
borderLeftWidth={1}
borderTopWidth={props.isFirst ? 1 : 0}
borderTopLeftRadius={props.isFirst ? 8 : 0}
borderBottomLeftRadius={props.isLast ? 8 : 0}
{...borders}
> >
<ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} /> <ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} />
</GridItem> </GridItem>
@@ -40,12 +34,8 @@ const ScenarioRow = (props: {
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
sx={isHovered ? highlightStyle : undefined} sx={isHovered ? highlightStyle : undefined}
bgColor="white"
rowStart={props.rowStart} rowStart={props.rowStart}
colStart={i + 2} colStart={i + 2}
borderTopWidth={props.isFirst ? 1 : 0}
borderTopRightRadius={props.isFirst && i === props.variants.length - 1 ? 8 : 0}
borderBottomRightRadius={props.isLast && i === props.variants.length - 1 ? 8 : 0}
{...borders} {...borders}
> >
<OutputCell key={variant.id} scenario={props.scenario} variant={variant} /> <OutputCell key={variant.id} scenario={props.scenario} variant={variant} />

View File

@@ -48,7 +48,7 @@ export const ScenariosHeader = () => {
); );
return ( return (
<HStack w="100%" py={cellPadding.y} px={cellPadding.x} align="center" spacing={0}> <HStack w="100%" pb={cellPadding.y} pt={0} align="center" spacing={0}>
<Text fontSize={16} fontWeight="bold"> <Text fontSize={16} fontWeight="bold">
Scenarios ({scenarios.data?.count}) Scenarios ({scenarios.data?.count})
</Text> </Text>
@@ -57,16 +57,11 @@ export const ScenariosHeader = () => {
<MenuButton <MenuButton
as={IconButton} as={IconButton}
mt={1} mt={1}
ml={2}
variant="ghost" variant="ghost"
aria-label="Edit Scenarios" aria-label="Edit Scenarios"
icon={<Icon as={loading ? Spinner : BsGear} />} icon={<Icon as={loading ? Spinner : BsGear} />}
maxW={8}
minW={8}
minH={8}
maxH={8}
/> />
<MenuList fontSize="md" zIndex="dropdown" mt={-1}> <MenuList fontSize="md" zIndex="dropdown" mt={-3}>
<MenuItem <MenuItem
icon={<Icon as={BsPlus} boxSize={6} mx="-5px" />} icon={<Icon as={BsPlus} boxSize={6} mx="-5px" />}
onClick={() => onAddScenario(false)} onClick={() => onAddScenario(false)}

View File

@@ -21,18 +21,14 @@ export default function VariantStats(props: { variant: PromptVariant }) {
outputTokens: 0, outputTokens: 0,
scenarioCount: 0, scenarioCount: 0,
outputCount: 0, outputCount: 0,
awaitingCompletions: false,
awaitingEvals: false, awaitingEvals: false,
}, },
refetchInterval, refetchInterval,
}, },
); );
// Poll every five seconds while we are waiting for LLM retrievals to finish // Poll every two seconds while we are waiting for LLM retrievals to finish
useEffect( useEffect(() => setRefetchInterval(data.awaitingEvals ? 5000 : 0), [data.awaitingEvals]);
() => setRefetchInterval(data.awaitingCompletions || data.awaitingEvals ? 5000 : 0),
[data.awaitingCompletions, data.awaitingEvals],
);
const [passColor, neutralColor, failColor] = useToken("colors", [ const [passColor, neutralColor, failColor] = useToken("colors", [
"green.500", "green.500",

View File

@@ -53,29 +53,20 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
colStart: i + 2, colStart: i + 2,
borderLeftWidth: i === 0 ? 1 : 0, borderLeftWidth: i === 0 ? 1 : 0,
marginLeft: i === 0 ? "-1px" : 0, marginLeft: i === 0 ? "-1px" : 0,
backgroundColor: "white", backgroundColor: "gray.100",
}; };
const isFirst = i === 0;
const isLast = i === variants.data.length - 1;
return ( return (
<Fragment key={variant.uiId}> <Fragment key={variant.uiId}>
<VariantHeader <VariantHeader
variant={variant} variant={variant}
canHide={variants.data.length > 1} canHide={variants.data.length > 1}
rowStart={1} rowStart={1}
borderTopLeftRadius={isFirst ? 8 : 0}
borderTopRightRadius={isLast ? 8 : 0}
{...sharedProps} {...sharedProps}
/> />
<GridItem rowStart={2} {...sharedProps}> <GridItem rowStart={2} {...sharedProps}>
<VariantEditor variant={variant} /> <VariantEditor variant={variant} />
</GridItem> </GridItem>
<GridItem <GridItem rowStart={3} {...sharedProps}>
rowStart={3}
{...sharedProps}
borderBottomLeftRadius={isFirst ? 8 : 0}
borderBottomRightRadius={isLast ? 8 : 0}
>
<VariantStats variant={variant} /> <VariantStats variant={variant} />
</GridItem> </GridItem>
</Fragment> </Fragment>
@@ -86,6 +77,7 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
colSpan={allCols - 1} colSpan={allCols - 1}
rowStart={variantHeaderRows + 1} rowStart={variantHeaderRows + 1}
colStart={1} colStart={1}
{...borders}
borderRightWidth={0} borderRightWidth={0}
> >
<ScenariosHeader /> <ScenariosHeader />
@@ -98,8 +90,6 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
scenario={scenario} scenario={scenario}
variants={variants.data} variants={variants.data}
canHide={visibleScenariosCount > 1} canHide={visibleScenariosCount > 1}
isFirst={i === 0}
isLast={i === visibleScenariosCount - 1}
/> />
))} ))}
<GridItem <GridItem

View File

@@ -1,117 +1,77 @@
import { HStack, IconButton, Text, Select, type StackProps, Icon } from "@chakra-ui/react"; import { Box, HStack, IconButton } from "@chakra-ui/react";
import React, { useCallback } from "react"; import {
import { FiChevronsLeft, FiChevronsRight, FiChevronLeft, FiChevronRight } from "react-icons/fi"; BsChevronDoubleLeft,
import { usePageParams } from "~/utils/hooks"; BsChevronDoubleRight,
BsChevronLeft,
const pageSizeOptions = [10, 25, 50, 100]; BsChevronRight,
} from "react-icons/bs";
import { usePage } from "~/utils/hooks";
const Paginator = ({ const Paginator = ({
numItemsLoaded,
startIndex,
lastPage,
count, count,
condense, }: {
...props numItemsLoaded: number;
}: { count: number; condense?: boolean } & StackProps) => { startIndex: number;
const { page, pageSize, setPageParams } = usePageParams(); lastPage: number;
count: number;
const lastPage = Math.ceil(count / pageSize); }) => {
const [page, setPage] = usePage();
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 = () => { const nextPage = () => {
if (page < lastPage) { if (page < lastPage) {
setPageParams({ page: page + 1 }, "replace"); setPage(page + 1, "replace");
} }
}; };
const prevPage = () => { const prevPage = () => {
if (page > 1) { if (page > 1) {
setPageParams({ page: page - 1 }, "replace"); setPage(page - 1, "replace");
} }
}; };
const goToLastPage = () => setPageParams({ page: lastPage }, "replace"); const goToLastPage = () => setPage(lastPage, "replace");
const goToFirstPage = () => setPageParams({ page: 1 }, "replace"); const goToFirstPage = () => setPage(1, "replace");
return ( return (
<HStack <HStack pt={4}>
pt={4} <IconButton
spacing={8} variant="ghost"
justifyContent={condense ? "flex-start" : "space-between"} size="sm"
alignItems="center" onClick={goToFirstPage}
w="full" isDisabled={page === 1}
{...props} aria-label="Go to first page"
> icon={<BsChevronDoubleLeft />}
{!condense && ( />
<> <IconButton
<HStack> variant="ghost"
<Text>Rows</Text> size="sm"
<Select onClick={prevPage}
value={pageSize} isDisabled={page === 1}
onChange={(e) => updatePageSize(parseInt(e.target.value))} aria-label="Previous page"
w={20} icon={<BsChevronLeft />}
backgroundColor="white" />
> <Box>
{pageSizeOptions.map((option) => ( {startIndex}-{startIndex + numItemsLoaded - 1} / {count}
<option key={option} value={option}> </Box>
{option} <IconButton
</option> variant="ghost"
))} size="sm"
</Select> onClick={nextPage}
</HStack> isDisabled={page === lastPage}
<Text> aria-label="Next page"
Page {page} of {lastPage} icon={<BsChevronRight />}
</Text> />
</> <IconButton
)} variant="ghost"
size="sm"
<HStack> onClick={goToLastPage}
<IconButton isDisabled={page === lastPage}
variant="outline" aria-label="Go to last page"
size="sm" icon={<BsChevronDoubleRight />}
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> </HStack>
); );
}; };

View File

@@ -75,7 +75,7 @@ export default function VariantHeader(
padding={0} padding={0}
sx={{ sx={{
position: "sticky", position: "sticky",
top: "-2", top: "0",
// Ensure that the menu always appears above the sticky header of other variants // Ensure that the menu always appears above the sticky header of other variants
zIndex: menuOpen ? "dropdown" : 10, zIndex: menuOpen ? "dropdown" : 10,
}} }}
@@ -84,7 +84,6 @@ export default function VariantHeader(
> >
<HStack <HStack
spacing={2} spacing={2}
py={2}
alignItems="flex-start" alignItems="flex-start"
minH={headerMinHeight} minH={headerMinHeight}
draggable={!isInputHovered} draggable={!isInputHovered}
@@ -103,9 +102,7 @@ export default function VariantHeader(
setIsDragTarget(false); setIsDragTarget(false);
}} }}
onDrop={onReorder} onDrop={onReorder}
backgroundColor={isDragTarget ? "gray.200" : "white"} backgroundColor={isDragTarget ? "gray.200" : "gray.100"}
borderTopLeftRadius={gridItemProps.borderTopLeftRadius}
borderTopRightRadius={gridItemProps.borderTopRightRadius}
h="full" h="full"
> >
<Icon <Icon

View File

@@ -0,0 +1,201 @@
import {
Box,
Card,
CardHeader,
Heading,
Table,
Tbody,
Td,
Th,
Thead,
Tr,
Tooltip,
Collapse,
HStack,
VStack,
IconButton,
useToast,
Icon,
Button,
ButtonGroup,
} from "@chakra-ui/react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { ChevronUpIcon, ChevronDownIcon, CopyIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { type RouterOutputs, api } from "~/utils/api";
import SyntaxHighlighter from "react-syntax-highlighter";
import { atelierCaveLight } from "react-syntax-highlighter/dist/cjs/styles/hljs";
import stringify from "json-stringify-pretty-compact";
import Link from "next/link";
dayjs.extend(relativeTime);
type LoggedCall = RouterOutputs["dashboard"]["loggedCalls"][0];
const FormattedJson = ({ json }: { json: any }) => {
const jsonString = stringify(json, { maxLength: 40 });
const toast = useToast();
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast({
title: "Copied to clipboard",
status: "success",
duration: 2000,
});
} catch (err) {
toast({
title: "Failed to copy to clipboard",
status: "error",
duration: 2000,
});
}
};
return (
<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>
);
};
function TableRow({
loggedCall,
isExpanded,
onToggle,
}: {
loggedCall: LoggedCall;
isExpanded: boolean;
onToggle: () => void;
}) {
const isError = loggedCall.modelResponse?.respStatus !== 200;
const timeAgo = dayjs(loggedCall.startTime).fromNow();
const fullTime = dayjs(loggedCall.startTime).toString();
const model = useMemo(
() => loggedCall.tags.find((tag) => tag.name.startsWith("$model"))?.value,
[loggedCall.tags],
);
return (
<>
<Tr
onClick={onToggle}
key={loggedCall.id}
_hover={{ bgColor: "gray.100", cursor: "pointer" }}
sx={{
"> td": { borderBottom: "none" },
}}
>
<Td>
<Icon boxSize={6} as={isExpanded ? ChevronUpIcon : ChevronDownIcon} />
</Td>
<Td>
<Tooltip label={fullTime} placement="top">
<Box whiteSpace="nowrap" minW="120px">
{timeAgo}
</Box>
</Tooltip>
</Td>
<Td width="100%">{model}</Td>
<Td isNumeric>{((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2)}s</Td>
<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?.respStatus ?? "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>
</>
);
}
export default function LoggedCallTable() {
const [expandedRow, setExpandedRow] = useState<string | null>(null);
const loggedCalls = api.dashboard.loggedCalls.useQuery({});
return (
<Card variant="outline" width="100%" overflow="hidden">
<CardHeader>
<Heading as="h3" size="sm">
Logged Calls
</Heading>
</CardHeader>
<Table>
<Thead>
<Tr>
<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>
<Tbody>
{loggedCalls.data?.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,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,16 +1,21 @@
import { type StackProps } from "@chakra-ui/react";
import { useDatasetEntries } from "~/utils/hooks"; import { useDatasetEntries } from "~/utils/hooks";
import Paginator from "../Paginator"; import Paginator from "../Paginator";
const DatasetEntriesPaginator = (props: StackProps) => { const DatasetEntriesPaginator = () => {
const { data } = useDatasetEntries(); const { data } = useDatasetEntries();
if (!data) return null; if (!data) return null;
const { count } = data; const { entries, startIndex, lastPage, count } = data;
return <Paginator count={count} {...props} />; return (
<Paginator
numItemsLoaded={entries.length}
startIndex={startIndex}
lastPage={lastPage}
count={count}
/>
);
}; };
export default DatasetEntriesPaginator; export default DatasetEntriesPaginator;

View File

@@ -7,7 +7,6 @@ import {
Spinner, Spinner,
AspectRatio, AspectRatio,
SkeletonText, SkeletonText,
Card,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { RiFlaskLine } from "react-icons/ri"; import { RiFlaskLine } from "react-icons/ri";
import { formatTimePast } from "~/utils/dayjs"; import { formatTimePast } from "~/utils/dayjs";
@@ -30,22 +29,17 @@ type ExperimentData = {
export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => { export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => {
return ( return (
<Card <AspectRatio ratio={1.2} w="full">
w="full"
h="full"
cursor="pointer"
p={4}
bg="white"
borderRadius={4}
_hover={{ bg: "gray.100" }}
transition="background 0.2s"
aspectRatio={1.2}
>
<VStack <VStack
as={Link} as={Link}
w="full"
h="full"
href={{ pathname: "/experiments/[id]", query: { id: exp.id } }} href={{ pathname: "/experiments/[id]", query: { id: exp.id } }}
bg="gray.50"
_hover={{ bg: "gray.100" }}
transition="background 0.2s"
cursor="pointer"
borderColor="gray.200"
borderWidth={1}
p={4}
justify="space-between" justify="space-between"
> >
<HStack w="full" color="gray.700" justify="center"> <HStack w="full" color="gray.700" justify="center">
@@ -63,7 +57,7 @@ export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => {
<Text flex={1}>Updated {formatTimePast(exp.updatedAt)}</Text> <Text flex={1}>Updated {formatTimePast(exp.updatedAt)}</Text>
</HStack> </HStack>
</VStack> </VStack>
</Card> </AspectRatio>
); );
}; };
@@ -95,30 +89,30 @@ export const NewExperimentCard = () => {
}, [createMutation, router, selectedProjectId]); }, [createMutation, router, selectedProjectId]);
return ( return (
<Card <AspectRatio ratio={1.2} w="full">
w="full" <VStack
h="full" align="center"
cursor="pointer" justify="center"
p={4} _hover={{ cursor: "pointer", bg: "gray.50" }}
bg="white" transition="background 0.2s"
borderRadius={4} cursor="pointer"
_hover={{ bg: "gray.100" }} borderColor="gray.200"
transition="background 0.2s" borderWidth={1}
aspectRatio={1.2} p={4}
> onClick={createExperiment}
<VStack align="center" justify="center" w="full" h="full" p={4} onClick={createExperiment}> >
<Icon as={isLoading ? Spinner : BsPlusSquare} boxSize={8} /> <Icon as={isLoading ? Spinner : BsPlusSquare} boxSize={8} />
<Text display={{ base: "none", md: "block" }} ml={2}> <Text display={{ base: "none", md: "block" }} ml={2}>
New Experiment New Experiment
</Text> </Text>
</VStack> </VStack>
</Card> </AspectRatio>
); );
}; };
export const ExperimentCardSkeleton = () => ( export const ExperimentCardSkeleton = () => (
<AspectRatio ratio={1.2} w="full"> <AspectRatio ratio={1.2} w="full">
<VStack align="center" borderColor="gray.200" borderWidth={1} p={4} bg="white"> <VStack align="center" borderColor="gray.200" borderWidth={1} p={4} bg="gray.50">
<SkeletonText noOfLines={1} w="80%" /> <SkeletonText noOfLines={1} w="80%" />
<SkeletonText noOfLines={2} w="60%" /> <SkeletonText noOfLines={2} w="60%" />
<SkeletonText noOfLines={1} w="80%" /> <SkeletonText noOfLines={1} w="80%" />

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect } from "react";
import { import {
Heading, Heading,
VStack, VStack,
@@ -9,14 +9,14 @@ import {
Box, Box,
Link as ChakraLink, Link as ChakraLink,
Flex, Flex,
useBreakpointValue,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { BsGearFill, BsGithub, BsPersonCircle } from "react-icons/bs"; import { BsGearFill, BsGithub, BsPersonCircle } from "react-icons/bs";
import { IoStatsChartOutline } from "react-icons/io5"; import { IoStatsChartOutline } from "react-icons/io5";
import { RiHome3Line, RiDatabase2Line, RiFlaskLine } from "react-icons/ri"; import { RiDatabase2Line, RiFlaskLine } from "react-icons/ri";
import { signIn, useSession } from "next-auth/react"; import { signIn, useSession } from "next-auth/react";
import UserMenu from "./UserMenu";
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
import ProjectMenu from "./ProjectMenu"; import ProjectMenu from "./ProjectMenu";
import NavSidebarOption from "./NavSidebarOption"; import NavSidebarOption from "./NavSidebarOption";
@@ -27,16 +27,10 @@ const Divider = () => <Box h="1px" bgColor="gray.300" w="full" />;
const NavSidebar = () => { const NavSidebar = () => {
const user = useSession().data; 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 ( return (
<VStack <VStack
align="stretch" align="stretch"
bgColor="gray.50"
py={2} py={2}
px={2} px={2}
pb={0} pb={0}
@@ -46,59 +40,32 @@ const NavSidebar = () => {
borderRightWidth={1} borderRightWidth={1}
borderColor="gray.300" borderColor="gray.300"
> >
{displayLogo && ( <HStack
<> as={Link}
<HStack href="/"
as={Link} _hover={{ textDecoration: "none" }}
href="/" spacing={{ base: 1, md: 0 }}
_hover={{ textDecoration: "none" }} mx={2}
spacing={{ base: 1, md: 0 }} py={{ base: 1, md: 2 }}
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">
<Image src="/logo.svg" alt="" boxSize={6} mr={4} ml={{ base: 0.5, md: 0 }} /> OpenPipe
<Heading size="md" fontFamily="inconsolata, monospace"> </Heading>
OpenPipe </HStack>
</Heading> <Divider />
</HStack>
<Divider />
</>
)}
<VStack align="flex-start" overflowY="auto" overflowX="hidden" flex={1}> <VStack align="flex-start" overflowY="auto" overflowX="hidden" flex={1}>
{user != null && ( {user != null && (
<> <>
<ProjectMenu /> <ProjectMenu />
<Divider /> <Divider />
{env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS && ( {env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS && (
<> <IconLink icon={IoStatsChartOutline} label="Logged Calls" href="/logged-calls" beta />
<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" /> <IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
{env.NEXT_PUBLIC_SHOW_DATA && ( {env.NEXT_PUBLIC_SHOW_DATA && (
<IconLink icon={RiDatabase2Line} label="Data" href="/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 && ( {user === null && (
@@ -120,7 +87,20 @@ const NavSidebar = () => {
</NavSidebarOption> </NavSidebarOption>
)} )}
</VStack> </VStack>
<VStack w="full" alignItems="flex-start" spacing={0}>
<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 && <UserMenu user={user} borderColor={"gray.200"} />}
<Divider /> <Divider />
<VStack spacing={0} align="center"> <VStack spacing={0} align="center">
<ChakraLink <ChakraLink
@@ -180,7 +160,7 @@ export default function AppShell({
<title>{title ? `${title} | OpenPipe` : "OpenPipe"}</title> <title>{title ? `${title} | OpenPipe` : "OpenPipe"}</title>
</Head> </Head>
<NavSidebar /> <NavSidebar />
<Box h="100%" flex={1} overflowY="auto" bgColor="gray.50"> <Box h="100%" flex={1} overflowY="auto">
{children} {children}
</Box> </Box>
</Flex> </Flex>

View File

@@ -6,18 +6,16 @@ import {
PopoverTrigger, PopoverTrigger,
PopoverContent, PopoverContent,
Flex, Flex,
IconButton,
Icon, Icon,
Divider, Divider,
Button, Button,
useDisclosure, useDisclosure,
Spinner, Spinner,
Link as ChakraLink,
Image,
Box,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { BsPlus, BsPersonCircle } from "react-icons/bs"; import { BsChevronRight, BsGear, BsPlus } from "react-icons/bs";
import { type Project } from "@prisma/client"; import { type Project } from "@prisma/client";
import { useAppStore } from "~/state/store"; import { useAppStore } from "~/state/store";
@@ -25,14 +23,13 @@ import { api } from "~/utils/api";
import NavSidebarOption from "./NavSidebarOption"; import NavSidebarOption from "./NavSidebarOption";
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks"; import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useSession, signOut } from "next-auth/react";
export default function ProjectMenu() { export default function ProjectMenu() {
const router = useRouter(); const router = useRouter();
const utils = api.useContext(); const utils = api.useContext();
const selectedProjectId = useAppStore((s) => s.selectedProjectId); const selectedProjectId = useAppStore((s) => s.selectedProjectId);
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId); const setselectedProjectId = useAppStore((s) => s.setselectedProjectId);
const { data: projects } = api.projects.list.useQuery(); const { data: projects } = api.projects.list.useQuery();
@@ -42,9 +39,9 @@ export default function ProjectMenu() {
projects[0] && projects[0] &&
(!selectedProjectId || !projects.find((proj) => proj.id === selectedProjectId)) (!selectedProjectId || !projects.find((proj) => proj.id === selectedProjectId))
) { ) {
setSelectedProjectId(projects[0].id); setselectedProjectId(projects[0].id);
} }
}, [selectedProjectId, setSelectedProjectId, projects]); }, [selectedProjectId, setselectedProjectId, projects]);
const { data: selectedProject } = useSelectedProject(); const { data: selectedProject } = useSelectedProject();
@@ -52,32 +49,28 @@ export default function ProjectMenu() {
const createMutation = api.projects.create.useMutation(); const createMutation = api.projects.create.useMutation();
const [createProject, isLoading] = useHandledAsyncCallback(async () => { const [createProject, isLoading] = useHandledAsyncCallback(async () => {
const newProj = await createMutation.mutateAsync({ name: "Untitled Project" }); const newProj = await createMutation.mutateAsync({ name: "New Project" });
await utils.projects.list.invalidate(); await utils.projects.list.invalidate();
setSelectedProjectId(newProj.id); setselectedProjectId(newProj.id);
await router.push({ pathname: "/project/settings" }); await router.push({ pathname: "/project/settings" });
}, [createMutation, router]); }, [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 ( return (
<VStack w="full" alignItems="flex-start" spacing={0} py={1}> <VStack w="full" alignItems="flex-start" spacing={0}>
<Popover <Text
placement="bottom" pl={2}
isOpen={popover.isOpen} pb={2}
onOpen={popover.onOpen} fontSize="xs"
onClose={popover.onClose} fontWeight="bold"
closeOnBlur color="gray.500"
display={{ base: "none", md: "flex" }}
> >
PROJECT
</Text>
<Popover placement="right-end" isOpen={popover.isOpen} onClose={popover.onClose} closeOnBlur>
<PopoverTrigger> <PopoverTrigger>
<NavSidebarOption> <NavSidebarOption>
<HStack w="full"> <HStack w="full" onClick={popover.onToggle}>
<Flex <Flex
p={1} p={1}
borderRadius={4} borderRadius={4}
@@ -90,35 +83,20 @@ export default function ProjectMenu() {
> >
<Text>{selectedProject?.name[0]?.toUpperCase()}</Text> <Text>{selectedProject?.name[0]?.toUpperCase()}</Text>
</Flex> </Flex>
<Text <Text fontSize="sm" display={{ base: "none", md: "block" }} py={1} flex={1}>
fontSize="sm"
display={{ base: "none", md: "block" }}
py={1}
flex={1}
fontWeight="bold"
>
{selectedProject?.name} {selectedProject?.name}
</Text> </Text>
<Box mr={2}>{profileImage}</Box> <Icon as={BsChevronRight} boxSize={4} color="gray.500" />
</HStack> </HStack>
</NavSidebarOption> </NavSidebarOption>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent _focusVisible={{ outline: "unset" }} ml={-1} w="auto" minW={100} maxW={280}>
_focusVisible={{ outline: "unset" }} <VStack alignItems="flex-start" spacing={2} py={4} px={2}>
ml={-1} <Text color="gray.500" fontSize="xs" fontWeight="bold" pb={1}>
w={224} PROJECTS
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> </Text>
<Divider /> <Divider />
<Text alignSelf="flex-start" fontWeight="bold" px={3} pt={2}> <VStack spacing={0} w="full">
Your Projects
</Text>
<VStack spacing={0} w="full" px={1}>
{projects?.map((proj) => ( {projects?.map((proj) => (
<ProjectOption <ProjectOption
key={proj.id} key={proj.id}
@@ -127,38 +105,19 @@ export default function ProjectMenu() {
onClose={popover.onClose} 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>
<HStack
as={Button}
variant="ghost"
colorScheme="blue"
color="blue.400"
pr={8}
w="full"
onClick={createProject}
>
<Icon as={isLoading ? Spinner : BsPlus} boxSize={6} />
<Text>New project</Text>
</HStack>
</VStack> </VStack>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
@@ -175,27 +134,38 @@ const ProjectOption = ({
isActive: boolean; isActive: boolean;
onClose: () => void; onClose: () => void;
}) => { }) => {
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId); const setselectedProjectId = useAppStore((s) => s.setselectedProjectId);
const [gearHovered, setGearHovered] = useState(false); const [gearHovered, setGearHovered] = useState(false);
return ( return (
<HStack <HStack
as={Link} as={Link}
href="/experiments" href="/experiments"
onClick={() => { onClick={() => {
setSelectedProjectId(proj.id); setselectedProjectId(proj.id);
onClose(); onClose();
}} }}
w="full" w="full"
justifyContent="space-between" justifyContent="space-between"
bgColor={isActive ? "gray.100" : "transparent"}
_hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }} _hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
color={isActive ? "blue.400" : undefined} p={2}
py={2}
px={4}
borderRadius={4} borderRadius={4}
spacing={4} spacing={4}
> >
<Text>{proj.name}</Text> <Text>{proj.name}</Text>
<IconButton
as={Link}
href="/project/settings"
aria-label={`Open ${proj.name} settings`}
icon={<Icon as={BsGear} boxSize={5} strokeWidth={0.5} color="gray.500" />}
variant="ghost"
size="xs"
p={0}
onMouseEnter={() => setGearHovered(true)}
onMouseLeave={() => setGearHovered(false)}
_hover={{ bgColor: isActive ? "gray.300" : "gray.100", transitionDelay: 0 }}
borderRadius={4}
/>
</HStack> </HStack>
); );
}; };

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,10 +1,56 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
import { isArray, isString } from "lodash-es"; import {
import { APIError } from "openai"; type ChatCompletionChunk,
import { type ChatCompletion, type CompletionCreateParams } from "openai/resources/chat"; type ChatCompletion,
import mergeChunks from "openpipe/src/openai/mergeChunks"; type CompletionCreateParams,
import { openai } from "~/server/utils/openai"; } from "openai/resources/chat";
import { type CompletionResponse } from "../types"; import { type CompletionResponse } from "../types";
import { isArray, isString, omit } from "lodash-es";
import { openai } from "~/server/utils/openai";
import { APIError } from "openai";
import frontendModelProvider from "./frontend";
import modelProvider, { type SupportedModel } from ".";
const mergeStreamedChunks = (
base: ChatCompletion | null,
chunk: ChatCompletionChunk,
): ChatCompletion => {
if (base === null) {
return mergeStreamedChunks({ ...chunk, choices: [] }, chunk);
}
const choices = [...base.choices];
for (const choice of chunk.choices) {
const baseChoice = choices.find((c) => c.index === choice.index);
if (baseChoice) {
baseChoice.finish_reason = choice.finish_reason ?? baseChoice.finish_reason;
baseChoice.message = baseChoice.message ?? { role: "assistant" };
if (choice.delta?.content)
baseChoice.message.content =
((baseChoice.message.content as string) ?? "") + (choice.delta.content ?? "");
if (choice.delta?.function_call) {
const fnCall = baseChoice.message.function_call ?? {};
fnCall.name =
((fnCall.name as string) ?? "") + ((choice.delta.function_call.name as string) ?? "");
fnCall.arguments =
((fnCall.arguments as string) ?? "") +
((choice.delta.function_call.arguments as string) ?? "");
}
} else {
// @ts-expect-error the types are correctly telling us that finish_reason
// could be null, but don't want to fix it right now.
choices.push({ ...omit(choice, "delta"), message: { role: "assistant", ...choice.delta } });
}
}
const merged: ChatCompletion = {
...base,
choices,
};
return merged;
};
export async function getCompletion( export async function getCompletion(
input: CompletionCreateParams, input: CompletionCreateParams,
@@ -15,6 +61,7 @@ export async function getCompletion(
try { try {
if (onStream) { if (onStream) {
console.log("got started");
const resp = await openai.chat.completions.create( const resp = await openai.chat.completions.create(
{ ...input, stream: true }, { ...input, stream: true },
{ {
@@ -22,9 +69,11 @@ export async function getCompletion(
}, },
); );
for await (const part of resp) { for await (const part of resp) {
finalCompletion = mergeChunks(finalCompletion, part); console.log("got part", part);
finalCompletion = mergeStreamedChunks(finalCompletion, part);
onStream(finalCompletion); onStream(finalCompletion);
} }
console.log("got final", finalCompletion);
if (!finalCompletion) { if (!finalCompletion) {
return { return {
type: "error", type: "error",

View File

@@ -120,9 +120,9 @@ export const refinementActions: Record<string, RefinementAction> = {
"Convert to function call": { "Convert to function call": {
icon: TfiThought, icon: TfiThought,
description: "Use function calls to get output from the model in a more structured way.", description: "Use function calls to get output from the model in a more structured way.",
instructions: `OpenAI functions are a specialized way for an LLM to return its final output. instructions: `OpenAI functions are a specialized way for an LLM to return output.
Example 1 before: This is what a prompt looks like before adding a function:
definePrompt("openai/ChatCompletion", { definePrompt("openai/ChatCompletion", {
model: "gpt-4", model: "gpt-4",
@@ -139,7 +139,7 @@ export const refinementActions: Record<string, RefinementAction> = {
], ],
}); });
Example 1 after: This is what one looks like after adding a function:
definePrompt("openai/ChatCompletion", { definePrompt("openai/ChatCompletion", {
model: "gpt-4", model: "gpt-4",
@@ -156,7 +156,7 @@ export const refinementActions: Record<string, RefinementAction> = {
], ],
functions: [ functions: [
{ {
name: "log_extracted_sentiment", name: "extract_sentiment",
parameters: { parameters: {
type: "object", // parameters must always be an object with a properties key type: "object", // parameters must always be an object with a properties key
properties: { // properties key is required properties: { // properties key is required
@@ -169,13 +169,13 @@ export const refinementActions: Record<string, RefinementAction> = {
}, },
], ],
function_call: { function_call: {
name: "log_extracted_sentiment", name: "extract_sentiment",
}, },
}); });
========= Here's another example of adding a function:
Example 2 before: Before:
definePrompt("openai/ChatCompletion", { definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo", model: "gpt-3.5-turbo",
@@ -197,7 +197,7 @@ export const refinementActions: Record<string, RefinementAction> = {
temperature: 0, temperature: 0,
}); });
Example 2 after: After:
definePrompt("openai/ChatCompletion", { definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo", model: "gpt-3.5-turbo",
@@ -215,7 +215,7 @@ export const refinementActions: Record<string, RefinementAction> = {
temperature: 0, temperature: 0,
functions: [ functions: [
{ {
name: "log_post_score", name: "score_post",
parameters: { parameters: {
type: "object", type: "object",
properties: { properties: {
@@ -227,13 +227,13 @@ export const refinementActions: Record<string, RefinementAction> = {
}, },
], ],
function_call: { function_call: {
name: "log_post_score", name: "score_post",
}, },
}); });
========= Another example
Example 3 before: Before:
definePrompt("openai/ChatCompletion", { definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo", model: "gpt-3.5-turbo",
@@ -246,7 +246,7 @@ export const refinementActions: Record<string, RefinementAction> = {
], ],
}); });
Example 3 after: After:
definePrompt("openai/ChatCompletion", { definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo", model: "gpt-3.5-turbo",
@@ -258,25 +258,22 @@ export const refinementActions: Record<string, RefinementAction> = {
], ],
functions: [ functions: [
{ {
name: "log_translated_text", name: "write_in_language",
parameters: { parameters: {
type: "object", type: "object",
properties: { properties: {
translated_text: { text: {
type: "string", type: "string",
description: "The text, written in the language specified in the prompt",
}, },
}, },
}, },
}, },
], ],
function_call: { function_call: {
name: "log_translated_text", name: "write_in_language",
}, },
}); });
=========
Add an OpenAI function that takes one or more nested parameters that match the expected output from this prompt.`, Add an OpenAI function that takes one or more nested parameters that match the expected output from this prompt.`,
}, },
}; };

View File

@@ -8,7 +8,7 @@ import { ChakraThemeProvider } from "~/theme/ChakraThemeProvider";
import { SyncAppStore } from "~/state/sync"; import { SyncAppStore } from "~/state/sync";
import NextAdapterApp from "next-query-params/app"; import NextAdapterApp from "next-query-params/app";
import { QueryParamProvider } from "use-query-params"; import { QueryParamProvider } from "use-query-params";
import { PosthogAppProvider } from "~/utils/analytics/posthog"; import { SessionIdentifier } from "~/utils/analytics/clientAnalytics";
const MyApp: AppType<{ session: Session | null }> = ({ const MyApp: AppType<{ session: Session | null }> = ({
Component, Component,
@@ -34,15 +34,14 @@ const MyApp: AppType<{ session: Session | null }> = ({
<meta name="twitter:image" content="/og.png" /> <meta name="twitter:image" content="/og.png" />
</Head> </Head>
<SessionProvider session={session}> <SessionProvider session={session}>
<PosthogAppProvider> <SyncAppStore />
<SyncAppStore /> <Favicon />
<Favicon /> <SessionIdentifier />
<ChakraThemeProvider> <ChakraThemeProvider>
<QueryParamProvider adapter={NextAdapterApp}> <QueryParamProvider adapter={NextAdapterApp}>
<Component {...pageProps} /> <Component {...pageProps} />
</QueryParamProvider> </QueryParamProvider>
</ChakraThemeProvider> </ChakraThemeProvider>
</PosthogAppProvider>
</SessionProvider> </SessionProvider>
</> </>
); );

View File

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

View File

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

View File

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

View File

@@ -62,7 +62,7 @@ export default function Experiment() {
useEffect(() => { useEffect(() => {
useAppStore.getState().sharedVariantEditor.loadMonaco().catch(console.error); useAppStore.getState().sharedVariantEditor.loadMonaco().catch(console.error);
}, []); });
const [label, setLabel] = useState(experiment.data?.label || ""); const [label, setLabel] = useState(experiment.data?.label || "");
useEffect(() => { useEffect(() => {

View File

@@ -15,16 +15,31 @@ import {
Tr, Tr,
Td, Td,
Divider, Divider,
Breadcrumb,
BreadcrumbItem,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
import { Ban, DollarSign, Hash } from "lucide-react"; import { Ban, DollarSign, Hash } from "lucide-react";
import { useMemo } from "react";
import AppShell from "~/components/nav/AppShell"; import AppShell from "~/components/nav/AppShell";
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
import { useSelectedProject } from "~/utils/hooks"; import { useSelectedProject } from "~/utils/hooks";
import dayjs from "~/utils/dayjs";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import LoggedCallsTable from "~/components/dashboard/LoggedCallsTable"; import LoggedCallTable from "~/components/dashboard/LoggedCallTable";
import UsageGraph from "~/components/dashboard/UsageGraph";
export default function Dashboard() { export default function LoggedCalls() {
const { data: selectedProject } = useSelectedProject(); const { data: selectedProject } = useSelectedProject();
const stats = api.dashboard.stats.useQuery( const stats = api.dashboard.stats.useQuery(
@@ -32,27 +47,79 @@ export default function Dashboard() {
{ enabled: !!selectedProject }, { enabled: !!selectedProject },
); );
const data = useMemo(() => {
return (
stats.data?.periods.map(({ period, numQueries, totalCost }) => ({
period,
Requests: numQueries,
"Total Spent (USD)": parseFloat(totalCost.toString()),
})) || []
);
}, [stats.data]);
return ( return (
<AppShell title="Dashboard" requireAuth> <AppShell requireAuth>
<VStack px={8} py={8} alignItems="flex-start" spacing={4}> <PageHeaderContainer>
<Breadcrumb>
<BreadcrumbItem>
<ProjectBreadcrumbContents />
</BreadcrumbItem>
<BreadcrumbItem isCurrentPage>
<Text>Logged Calls</Text>
</BreadcrumbItem>
</Breadcrumb>
</PageHeaderContainer>
<VStack px={8} pt={4} alignItems="flex-start" spacing={4}>
<Text fontSize="2xl" fontWeight="bold"> <Text fontSize="2xl" fontWeight="bold">
Dashboard {selectedProject?.name}
</Text> </Text>
<Divider /> <Divider />
<VStack margin="auto" spacing={4} align="stretch" w="full"> <VStack margin="auto" spacing={4} align="stretch" w="full">
<HStack gap={4} align="start"> <HStack gap={4} align="start">
<Card flex={1}> <Card variant="outline" flex={1}>
<CardHeader> <CardHeader>
<Heading as="h3" size="sm"> <Heading as="h3" size="sm">
Usage Statistics Usage Statistics
</Heading> </Heading>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<UsageGraph /> <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>
</CardBody> </CardBody>
</Card> </Card>
<VStack spacing="4" width="300px" align="stretch"> <VStack spacing="4" width="300px" align="stretch">
<Card> <Card variant="outline">
<CardBody> <CardBody>
<Stat> <Stat>
<HStack> <HStack>
@@ -60,12 +127,12 @@ export default function Dashboard() {
<Icon as={DollarSign} boxSize={4} color="gray.500" /> <Icon as={DollarSign} boxSize={4} color="gray.500" />
</HStack> </HStack>
<StatNumber> <StatNumber>
${parseFloat(stats.data?.totals?.cost?.toString() ?? "0").toFixed(3)} ${parseFloat(stats.data?.totals?.totalCost?.toString() ?? "0").toFixed(2)}
</StatNumber> </StatNumber>
</Stat> </Stat>
</CardBody> </CardBody>
</Card> </Card>
<Card> <Card variant="outline">
<CardBody> <CardBody>
<Stat> <Stat>
<HStack> <HStack>
@@ -80,7 +147,7 @@ export default function Dashboard() {
</Stat> </Stat>
</CardBody> </CardBody>
</Card> </Card>
<Card overflow="hidden"> <Card variant="outline" overflow="hidden">
<Stat> <Stat>
<CardHeader> <CardHeader>
<HStack> <HStack>
@@ -106,7 +173,7 @@ export default function Dashboard() {
</Card> </Card>
</VStack> </VStack>
</HStack> </HStack>
<LoggedCallsTable /> <LoggedCallTable />
</VStack> </VStack>
</VStack> </VStack>
</AppShell> </AppShell>

View File

@@ -38,10 +38,7 @@ export default function Settings() {
id: selectedProject.id, id: selectedProject.id,
updates: { name }, updates: { name },
}); });
await Promise.all([ await Promise.all([utils.projects.get.invalidate({ id: selectedProject.id })]);
utils.projects.get.invalidate({ id: selectedProject.id }),
utils.projects.list.invalidate(),
]);
} }
}, [updateMutation, selectedProject]); }, [updateMutation, selectedProject]);
@@ -65,7 +62,7 @@ export default function Settings() {
</BreadcrumbItem> </BreadcrumbItem>
</Breadcrumb> </Breadcrumb>
</PageHeaderContainer> </PageHeaderContainer>
<VStack px={8} py={4} alignItems="flex-start" spacing={4}> <VStack px={8} pt={4} alignItems="flex-start" spacing={4}>
<VStack spacing={0} alignItems="flex-start"> <VStack spacing={0} alignItems="flex-start">
<Text fontSize="2xl" fontWeight="bold"> <Text fontSize="2xl" fontWeight="bold">
Project Settings Project Settings
@@ -80,7 +77,6 @@ export default function Settings() {
borderWidth={1} borderWidth={1}
borderRadius={4} borderRadius={4}
borderColor="gray.300" borderColor="gray.300"
bgColor="white"
p={6} p={6}
spacing={6} spacing={6}
> >

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

@@ -4,15 +4,11 @@ import parserTypescript from "prettier/plugins/typescript";
// @ts-expect-error for some reason missing from types // @ts-expect-error for some reason missing from types
import parserEstree from "prettier/plugins/estree"; import parserEstree from "prettier/plugins/estree";
// This emits a warning in the browser "Critical dependency: the request of a
// dependency is an expression". Unfortunately doesn't seem to be a way to get
// around it if we want to use Babel client-side for now. One solution would be
// to just do the formatting server-side in a trpc call.
// https://github.com/babel/babel/issues/14301
import * as babel from "@babel/standalone"; import * as babel from "@babel/standalone";
export function stripTypes(tsCode: string): string { export function stripTypes(tsCode: string): string {
const options = { const options = {
presets: ["typescript"],
filename: "file.ts", filename: "file.ts",
}; };

View File

@@ -1,95 +0,0 @@
import type { ApiKey, Project } from "@prisma/client";
import { TRPCError, initTRPC } from "@trpc/server";
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
import superjson from "superjson";
import { type OpenApiMeta } from "trpc-openapi";
import { ZodError } from "zod";
import { prisma } from "~/server/db";
type CreateContextOptions = {
key:
| (ApiKey & {
project: Project;
})
| null;
};
/**
* This helper generates the "internals" for a tRPC context. If you need to use it, you can export
* it from here.
*
* Examples of things you may need it for:
* - testing, so we don't have to mock Next.js' req/res
* - tRPC's `createSSGHelpers`, where we don't have req/res
*
* @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
*/
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
return {
key: opts.key,
};
};
export const createOpenApiContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;
const apiKey = req.headers.authorization?.split(" ")[1] as string | null;
if (!apiKey) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const key = await prisma.apiKey.findUnique({
where: { apiKey },
include: { project: true },
});
if (!key) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return createInnerTRPCContext({
key,
});
};
export type TRPCContext = Awaited<ReturnType<typeof createOpenApiContext>>;
const t = initTRPC
.context<typeof createOpenApiContext>()
.meta<OpenApiMeta>()
.create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
export const createOpenApiRouter = t.router;
export const openApiPublicProc = t.procedure;
/** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceApiKey = t.middleware(async ({ ctx, next }) => {
if (!ctx.key) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: { key: ctx.key },
});
});
/**
* Protected (authenticated) procedure
*
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
* the session is valid and guarantees `ctx.session.user` is not null.
*
* @see https://trpc.io/docs/procedures
*/
export const openApiProtectedProc = t.procedure.use(enforceApiKey);

View File

@@ -1,241 +0,0 @@
import { type Prisma } from "@prisma/client";
import { type JsonValue } from "type-fest";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { prisma } from "~/server/db";
import { hashRequest } from "~/server/utils/hashObject";
import modelProvider from "~/modelProviders/openai-ChatCompletion";
import {
type ChatCompletion,
type CompletionCreateParams,
} from "openai/resources/chat/completions";
import { createOpenApiRouter, openApiProtectedProc } from "./openApiTrpc";
const reqValidator = z.object({
model: z.string(),
messages: z.array(z.any()),
});
const respValidator = z.object({
id: z.string(),
model: z.string(),
choices: z.array(
z.object({
finish_reason: z.string(),
}),
),
});
export const v1ApiRouter = createOpenApiRouter({
checkCache: openApiProtectedProc
.meta({
openapi: {
method: "POST",
path: "/check-cache",
description: "Check if a prompt is cached",
protect: true,
},
})
.input(
z.object({
requestedAt: z.number().describe("Unix timestamp in milliseconds"),
reqPayload: z.unknown().describe("JSON-encoded request payload"),
tags: z
.record(z.string())
.optional()
.describe(
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
)
.default({}),
}),
)
.output(
z.object({
respPayload: z.unknown().optional().describe("JSON-encoded response payload"),
}),
)
.mutation(async ({ input, ctx }) => {
const reqPayload = await reqValidator.spa(input.reqPayload);
const cacheKey = hashRequest(ctx.key.projectId, reqPayload as JsonValue);
const existingResponse = await prisma.loggedCallModelResponse.findFirst({
where: { cacheKey },
include: { originalLoggedCall: true },
orderBy: { requestedAt: "desc" },
});
if (!existingResponse) return { respPayload: null };
await prisma.loggedCall.create({
data: {
projectId: ctx.key.projectId,
requestedAt: new Date(input.requestedAt),
cacheHit: true,
modelResponseId: existingResponse.id,
},
});
await createTags(existingResponse.originalLoggedCallId, input.tags);
return {
respPayload: existingResponse.respPayload,
};
}),
report: openApiProtectedProc
.meta({
openapi: {
method: "POST",
path: "/report",
description: "Report an API call",
protect: true,
},
})
.input(
z.object({
requestedAt: z.number().describe("Unix timestamp in milliseconds"),
receivedAt: z.number().describe("Unix timestamp in milliseconds"),
reqPayload: z.unknown().describe("JSON-encoded request payload"),
respPayload: z.unknown().optional().describe("JSON-encoded response payload"),
statusCode: z.number().optional().describe("HTTP status code of response"),
errorMessage: z.string().optional().describe("User-friendly error message"),
tags: z
.record(z.string())
.optional()
.describe(
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
)
.default({}),
}),
)
.output(z.object({ status: z.literal("ok") }))
.mutation(async ({ input, ctx }) => {
const reqPayload = await reqValidator.spa(input.reqPayload);
const respPayload = await respValidator.spa(input.respPayload);
const requestHash = hashRequest(ctx.key.projectId, reqPayload as JsonValue);
const newLoggedCallId = uuidv4();
const newModelResponseId = uuidv4();
let usage;
let model;
if (reqPayload.success && respPayload.success) {
usage = modelProvider.getUsage(
input.reqPayload as CompletionCreateParams,
input.respPayload as ChatCompletion,
);
model = reqPayload.data.model;
}
await prisma.$transaction([
prisma.loggedCall.create({
data: {
id: newLoggedCallId,
projectId: ctx.key.projectId,
requestedAt: new Date(input.requestedAt),
cacheHit: false,
model,
},
}),
prisma.loggedCallModelResponse.create({
data: {
id: newModelResponseId,
originalLoggedCallId: newLoggedCallId,
requestedAt: new Date(input.requestedAt),
receivedAt: new Date(input.receivedAt),
reqPayload: input.reqPayload as Prisma.InputJsonValue,
respPayload: input.respPayload as Prisma.InputJsonValue,
statusCode: input.statusCode,
errorMessage: input.errorMessage,
durationMs: input.receivedAt - input.requestedAt,
cacheKey: respPayload.success ? requestHash : null,
inputTokens: usage?.inputTokens,
outputTokens: usage?.outputTokens,
cost: usage?.cost,
},
}),
// Avoid foreign key constraint error by updating the logged call after the model response is created
prisma.loggedCall.update({
where: {
id: newLoggedCallId,
},
data: {
modelResponseId: newModelResponseId,
},
}),
]);
await createTags(newLoggedCallId, input.tags);
return { status: "ok" };
}),
localTestingOnlyGetLatestLoggedCall: openApiProtectedProc
.meta({
openapi: {
method: "GET",
path: "/local-testing-only-get-latest-logged-call",
description: "Get the latest logged call (only for local testing)",
protect: true, // Make sure to protect this endpoint
},
})
.input(z.void())
.output(
z
.object({
createdAt: z.date(),
cacheHit: z.boolean(),
tags: z.record(z.string().nullable()),
modelResponse: z
.object({
id: z.string(),
statusCode: z.number().nullable(),
errorMessage: z.string().nullable(),
reqPayload: z.unknown(),
respPayload: z.unknown(),
})
.nullable(),
})
.nullable(),
)
.mutation(async ({ ctx }) => {
if (process.env.NODE_ENV === "production") {
throw new Error("This operation is not allowed in production environment");
}
const latestLoggedCall = await prisma.loggedCall.findFirst({
where: { projectId: ctx.key.projectId },
orderBy: { requestedAt: "desc" },
select: {
createdAt: true,
cacheHit: true,
tags: true,
modelResponse: {
select: {
id: true,
statusCode: true,
errorMessage: true,
reqPayload: true,
respPayload: true,
},
},
},
});
return (
latestLoggedCall && {
...latestLoggedCall,
tags: Object.fromEntries(latestLoggedCall.tags.map((tag) => [tag.name, tag.value])),
}
);
}),
});
async function createTags(loggedCallId: string, tags: Record<string, string>) {
const tagsToCreate = Object.entries(tags).map(([name, value]) => ({
loggedCallId,
name: name.replaceAll(/[^a-zA-Z0-9_$]/g, "_"),
value,
}));
await prisma.loggedCallTag.createMany({
data: tagsToCreate,
});
}

View File

@@ -8,9 +8,9 @@ import { evaluationsRouter } from "./routers/evaluations.router";
import { worldChampsRouter } from "./routers/worldChamps.router"; import { worldChampsRouter } from "./routers/worldChamps.router";
import { datasetsRouter } from "./routers/datasets.router"; import { datasetsRouter } from "./routers/datasets.router";
import { datasetEntries } from "./routers/datasetEntries.router"; import { datasetEntries } from "./routers/datasetEntries.router";
import { externalApiRouter } from "./routers/externalApi.router";
import { projectsRouter } from "./routers/projects.router"; import { projectsRouter } from "./routers/projects.router";
import { dashboardRouter } from "./routers/dashboard.router"; import { dashboardRouter } from "./routers/dashboard.router";
import { loggedCallsRouter } from "./routers/loggedCalls.router";
/** /**
* This is the primary router for your server. * This is the primary router for your server.
@@ -29,7 +29,7 @@ export const appRouter = createTRPCRouter({
datasetEntries: datasetEntries, datasetEntries: datasetEntries,
projects: projectsRouter, projects: projectsRouter,
dashboard: dashboardRouter, dashboard: dashboardRouter,
loggedCalls: loggedCallsRouter, externalApi: externalApiRouter,
}); });
// export type definition of API // export type definition of API

View File

@@ -1,12 +1,11 @@
import { sql } from "kysely"; import { sql } from "kysely";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { kysely } from "~/server/db"; import { kysely, prisma } from "~/server/db";
import { requireCanViewProject } from "~/utils/accessControl";
import dayjs from "~/utils/dayjs"; import dayjs from "~/utils/dayjs";
export const dashboardRouter = createTRPCRouter({ export const dashboardRouter = createTRPCRouter({
stats: protectedProcedure stats: publicProcedure
.input( .input(
z.object({ z.object({
// TODO: actually take startDate into account // TODO: actually take startDate into account
@@ -14,8 +13,7 @@ export const dashboardRouter = createTRPCRouter({
projectId: z.string(), projectId: z.string(),
}), }),
) )
.query(async ({ input, ctx }) => { .query(async ({ input }) => {
await requireCanViewProject(input.projectId, ctx);
// Return the stats group by hour // Return the stats group by hour
const periods = await kysely const periods = await kysely
.selectFrom("LoggedCall") .selectFrom("LoggedCall")
@@ -26,9 +24,9 @@ export const dashboardRouter = createTRPCRouter({
) )
.where("projectId", "=", input.projectId) .where("projectId", "=", input.projectId)
.select(({ fn }) => [ .select(({ fn }) => [
sql<Date>`date_trunc('day', "LoggedCallModelResponse"."requestedAt")`.as("period"), sql<Date>`date_trunc('day', "LoggedCallModelResponse"."startTime")`.as("period"),
sql<number>`count("LoggedCall"."id")::int`.as("numQueries"), sql<number>`count("LoggedCall"."id")::int`.as("numQueries"),
fn.sum(fn.coalesce("LoggedCallModelResponse.cost", sql<number>`0`)).as("cost"), fn.sum(fn.coalesce("LoggedCallModelResponse.totalCost", sql<number>`0`)).as("totalCost"),
]) ])
.groupBy("period") .groupBy("period")
.orderBy("period") .orderBy("period")
@@ -59,7 +57,7 @@ export const dashboardRouter = createTRPCRouter({
backfilledPeriods.unshift({ backfilledPeriods.unshift({
period: dayjs(dayToMatch).toDate(), period: dayjs(dayToMatch).toDate(),
numQueries: 0, numQueries: 0,
cost: 0, totalCost: 0,
}); });
} }
dayToMatch = dayToMatch.subtract(1, "day"); dayToMatch = dayToMatch.subtract(1, "day");
@@ -74,7 +72,7 @@ export const dashboardRouter = createTRPCRouter({
) )
.where("projectId", "=", input.projectId) .where("projectId", "=", input.projectId)
.select(({ fn }) => [ .select(({ fn }) => [
fn.sum(fn.coalesce("LoggedCallModelResponse.cost", sql<number>`0`)).as("cost"), fn.sum(fn.coalesce("LoggedCallModelResponse.totalCost", sql<number>`0`)).as("totalCost"),
fn.count("LoggedCall.id").as("numQueries"), fn.count("LoggedCall.id").as("numQueries"),
]) ])
.executeTakeFirst(); .executeTakeFirst();
@@ -87,8 +85,8 @@ export const dashboardRouter = createTRPCRouter({
"LoggedCall.id", "LoggedCall.id",
"LoggedCallModelResponse.originalLoggedCallId", "LoggedCallModelResponse.originalLoggedCallId",
) )
.select(({ fn }) => [fn.count("LoggedCall.id").as("count"), "statusCode as code"]) .select(({ fn }) => [fn.count("LoggedCall.id").as("count"), "respStatus as code"])
.where("statusCode", ">", 200) .where("respStatus", ">", 200)
.groupBy("code") .groupBy("code")
.orderBy("count", "desc") .orderBy("count", "desc")
.execute(); .execute();
@@ -105,4 +103,16 @@ export const dashboardRouter = createTRPCRouter({
return { periods: backfilledPeriods, totals, errors: namedErrors }; return { periods: backfilledPeriods, totals, errors: namedErrors };
}), }),
// TODO useInfiniteQuery
// https://discord.com/channels/966627436387266600/1122258443886153758/1122258443886153758
loggedCalls: publicProcedure.input(z.object({})).query(async ({ input }) => {
const loggedCalls = await prisma.loggedCall.findMany({
orderBy: { startTime: "desc" },
include: { tags: true, modelResponse: true },
take: 20,
});
return loggedCalls;
}),
}); });

View File

@@ -4,21 +4,23 @@ import { prisma } from "~/server/db";
import { requireCanModifyDataset, requireCanViewDataset } from "~/utils/accessControl"; import { requireCanModifyDataset, requireCanViewDataset } from "~/utils/accessControl";
import { autogenerateDatasetEntries } from "../autogenerate/autogenerateDatasetEntries"; import { autogenerateDatasetEntries } from "../autogenerate/autogenerateDatasetEntries";
const PAGE_SIZE = 10;
export const datasetEntries = createTRPCRouter({ export const datasetEntries = createTRPCRouter({
list: protectedProcedure list: protectedProcedure
.input(z.object({ datasetId: z.string(), page: z.number(), pageSize: z.number() })) .input(z.object({ datasetId: z.string(), page: z.number() }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await requireCanViewDataset(input.datasetId, ctx); await requireCanViewDataset(input.datasetId, ctx);
const { datasetId, page, pageSize } = input; const { datasetId, page } = input;
const entries = await prisma.datasetEntry.findMany({ const entries = await prisma.datasetEntry.findMany({
where: { where: {
datasetId, datasetId,
}, },
orderBy: { createdAt: "desc" }, orderBy: { createdAt: "desc" },
skip: (page - 1) * pageSize, skip: (page - 1) * PAGE_SIZE,
take: pageSize, take: PAGE_SIZE,
}); });
const count = await prisma.datasetEntry.count({ const count = await prisma.datasetEntry.count({
@@ -29,6 +31,8 @@ export const datasetEntries = createTRPCRouter({
return { return {
entries, entries,
startIndex: (page - 1) * PAGE_SIZE + 1,
lastPage: Math.ceil(count / PAGE_SIZE),
count, count,
}; };
}), }),

View File

@@ -227,7 +227,7 @@ export const experimentsRouter = createTRPCRouter({
...modelResponseData, ...modelResponseData,
id: newModelResponseId, id: newModelResponseId,
scenarioVariantCellId: newCellId, scenarioVariantCellId: newCellId,
respPayload: (modelResponse.respPayload as Prisma.InputJsonValue) ?? undefined, output: (modelResponse.output as Prisma.InputJsonValue) ?? undefined,
}); });
for (const evaluation of outputEvaluations) { for (const evaluation of outputEvaluations) {
outputEvaluationsToCreate.push({ outputEvaluationsToCreate.push({

View File

@@ -0,0 +1,205 @@
import { type Prisma } from "@prisma/client";
import { type JsonValue } from "type-fest";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { prisma } from "~/server/db";
import { hashRequest } from "~/server/utils/hashObject";
const reqValidator = z.object({
model: z.string(),
messages: z.array(z.any()),
});
const respValidator = z.object({
id: z.string(),
model: z.string(),
usage: z.object({
total_tokens: z.number(),
prompt_tokens: z.number(),
completion_tokens: z.number(),
}),
choices: z.array(
z.object({
finish_reason: z.string(),
}),
),
});
export const externalApiRouter = createTRPCRouter({
checkCache: publicProcedure
.meta({
openapi: {
method: "POST",
path: "/v1/check-cache",
description: "Check if a prompt is cached",
},
})
.input(
z.object({
startTime: z.number().describe("Unix timestamp in milliseconds"),
reqPayload: z.unknown().describe("JSON-encoded request payload"),
tags: z
.record(z.string())
.optional()
.describe(
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
),
}),
)
.output(
z.object({
respPayload: z.unknown().optional().describe("JSON-encoded response payload"),
}),
)
.mutation(async ({ input, ctx }) => {
const apiKey = ctx.apiKey;
if (!apiKey) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const key = await prisma.apiKey.findUnique({
where: { apiKey },
});
if (!key) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const reqPayload = await reqValidator.spa(input.reqPayload);
const cacheKey = hashRequest(key.projectId, reqPayload as JsonValue);
const existingResponse = await prisma.loggedCallModelResponse.findFirst({
where: {
cacheKey,
},
include: {
originalLoggedCall: true,
},
orderBy: {
startTime: "desc",
},
});
if (!existingResponse) return { respPayload: null };
await prisma.loggedCall.create({
data: {
projectId: key.projectId,
startTime: new Date(input.startTime),
cacheHit: true,
modelResponseId: existingResponse.id,
},
});
return {
respPayload: existingResponse.respPayload,
};
}),
report: publicProcedure
.meta({
openapi: {
method: "POST",
path: "/v1/report",
description: "Report an API call",
},
})
.input(
z.object({
startTime: z.number().describe("Unix timestamp in milliseconds"),
endTime: z.number().describe("Unix timestamp in milliseconds"),
reqPayload: z.unknown().describe("JSON-encoded request payload"),
respPayload: z.unknown().optional().describe("JSON-encoded response payload"),
respStatus: z.number().optional().describe("HTTP status code of response"),
error: z.string().optional().describe("User-friendly error message"),
tags: z
.record(z.string())
.optional()
.describe(
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
),
}),
)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const apiKey = ctx.apiKey;
if (!apiKey) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const key = await prisma.apiKey.findUnique({
where: { apiKey },
});
if (!key) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const reqPayload = await reqValidator.spa(input.reqPayload);
const respPayload = await respValidator.spa(input.respPayload);
const requestHash = hashRequest(key.projectId, reqPayload as JsonValue);
const newLoggedCallId = uuidv4();
const newModelResponseId = uuidv4();
const usage = respPayload.success ? respPayload.data.usage : undefined;
await prisma.$transaction([
prisma.loggedCall.create({
data: {
id: newLoggedCallId,
projectId: key.projectId,
startTime: new Date(input.startTime),
cacheHit: false,
},
}),
prisma.loggedCallModelResponse.create({
data: {
id: newModelResponseId,
originalLoggedCallId: newLoggedCallId,
startTime: new Date(input.startTime),
endTime: new Date(input.endTime),
reqPayload: input.reqPayload as Prisma.InputJsonValue,
respPayload: input.respPayload as Prisma.InputJsonValue,
respStatus: input.respStatus,
error: input.error,
durationMs: input.endTime - input.startTime,
...(respPayload.success
? {
cacheKey: requestHash,
inputTokens: usage ? usage.prompt_tokens : undefined,
outputTokens: usage ? usage.completion_tokens : undefined,
}
: null),
},
}),
// Avoid foreign key constraint error by updating the logged call after the model response is created
prisma.loggedCall.update({
where: {
id: newLoggedCallId,
},
data: {
modelResponseId: newModelResponseId,
},
}),
]);
if (input.tags) {
const tagsToCreate = Object.entries(input.tags).map(([name, value]) => ({
loggedCallId: newLoggedCallId,
// sanitize tags
name: name.replaceAll(/[^a-zA-Z0-9_]/g, "_"),
value,
}));
if (reqPayload.success) {
tagsToCreate.push({
loggedCallId: newLoggedCallId,
name: "$model",
value: reqPayload.data.model,
});
}
await prisma.loggedCallTag.createMany({
data: tagsToCreate,
});
}
}),
});

View File

@@ -1,33 +0,0 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { prisma } from "~/server/db";
import { requireCanViewProject } from "~/utils/accessControl";
export const loggedCallsRouter = createTRPCRouter({
list: protectedProcedure
.input(z.object({ projectId: z.string(), page: z.number(), pageSize: z.number() }))
.query(async ({ input, ctx }) => {
const { projectId, page, pageSize } = input;
await requireCanViewProject(projectId, ctx);
const calls = await prisma.loggedCall.findMany({
where: { projectId },
orderBy: { requestedAt: "desc" },
include: { tags: true, modelResponse: true },
skip: (page - 1) * pageSize,
take: pageSize,
});
const matchingLogs = await prisma.loggedCall.findMany({
where: { projectId },
select: { id: true },
});
const count = await prisma.loggedCall.count({
where: { projectId },
});
return { calls, count, matchingLogIds: matchingLogs.map((log) => log.id) };
}),
});

View File

@@ -3,7 +3,7 @@ import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/
import { prisma } from "~/server/db"; import { prisma } from "~/server/db";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { generateNewCell } from "~/server/utils/generateNewCell"; import { generateNewCell } from "~/server/utils/generateNewCell";
import { error, success } from "~/utils/errorHandling/standardResponses"; import { error, success } from "~/utils/standardResponses";
import { recordExperimentUpdated } from "~/server/utils/recordExperimentUpdated"; import { recordExperimentUpdated } from "~/server/utils/recordExperimentUpdated";
import { reorderPromptVariants } from "~/server/utils/reorderPromptVariants"; import { reorderPromptVariants } from "~/server/utils/reorderPromptVariants";
import { type PromptVariant } from "@prisma/client"; import { type PromptVariant } from "@prisma/client";
@@ -55,7 +55,7 @@ export const promptVariantsRouter = createTRPCRouter({
where: { where: {
modelResponse: { modelResponse: {
outdated: false, outdated: false,
respPayload: { not: Prisma.AnyNull }, output: { not: Prisma.AnyNull },
scenarioVariantCell: { scenarioVariantCell: {
promptVariant: { promptVariant: {
id: input.variantId, id: input.variantId,
@@ -100,7 +100,7 @@ export const promptVariantsRouter = createTRPCRouter({
modelResponses: { modelResponses: {
some: { some: {
outdated: false, outdated: false,
respPayload: { output: {
not: Prisma.AnyNull, not: Prisma.AnyNull,
}, },
}, },
@@ -111,7 +111,7 @@ export const promptVariantsRouter = createTRPCRouter({
const overallTokens = await prisma.modelResponse.aggregate({ const overallTokens = await prisma.modelResponse.aggregate({
where: { where: {
outdated: false, outdated: false,
respPayload: { output: {
not: Prisma.AnyNull, not: Prisma.AnyNull,
}, },
scenarioVariantCell: { scenarioVariantCell: {
@@ -131,8 +131,6 @@ export const promptVariantsRouter = createTRPCRouter({
const inputTokens = overallTokens._sum?.inputTokens ?? 0; const inputTokens = overallTokens._sum?.inputTokens ?? 0;
const outputTokens = overallTokens._sum?.outputTokens ?? 0; const outputTokens = overallTokens._sum?.outputTokens ?? 0;
const awaitingCompletions = outputCount < scenarioCount;
const awaitingEvals = !!evalResults.find( const awaitingEvals = !!evalResults.find(
(result) => result.totalCount < scenarioCount * evals.length, (result) => result.totalCount < scenarioCount * evals.length,
); );
@@ -144,7 +142,6 @@ export const promptVariantsRouter = createTRPCRouter({
overallCost: overallTokens._sum?.cost ?? 0, overallCost: overallTokens._sum?.cost ?? 0,
scenarioCount, scenarioCount,
outputCount, outputCount,
awaitingCompletions,
awaitingEvals, awaitingEvals,
}; };
}), }),

View File

@@ -3,7 +3,7 @@ import { sql } from "kysely";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc";
import { kysely, prisma } from "~/server/db"; import { kysely, prisma } from "~/server/db";
import { error, success } from "~/utils/errorHandling/standardResponses"; import { error, success } from "~/utils/standardResponses";
import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl"; import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl";
export const scenarioVarsRouter = createTRPCRouter({ export const scenarioVarsRouter = createTRPCRouter({

View File

@@ -7,13 +7,15 @@ import { runAllEvals } from "~/server/utils/evaluations";
import { generateNewCell } from "~/server/utils/generateNewCell"; import { generateNewCell } from "~/server/utils/generateNewCell";
import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl"; import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl";
const PAGE_SIZE = 10;
export const scenariosRouter = createTRPCRouter({ export const scenariosRouter = createTRPCRouter({
list: publicProcedure list: publicProcedure
.input(z.object({ experimentId: z.string(), page: z.number(), pageSize: z.number() })) .input(z.object({ experimentId: z.string(), page: z.number() }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await requireCanViewExperiment(input.experimentId, ctx); await requireCanViewExperiment(input.experimentId, ctx);
const { experimentId, page, pageSize } = input; const { experimentId, page } = input;
const scenarios = await prisma.testScenario.findMany({ const scenarios = await prisma.testScenario.findMany({
where: { where: {
@@ -21,8 +23,8 @@ export const scenariosRouter = createTRPCRouter({
visible: true, visible: true,
}, },
orderBy: { sortIndex: "asc" }, orderBy: { sortIndex: "asc" },
skip: (page - 1) * pageSize, skip: (page - 1) * PAGE_SIZE,
take: pageSize, take: PAGE_SIZE,
}); });
const count = await prisma.testScenario.count({ const count = await prisma.testScenario.count({
@@ -34,6 +36,8 @@ export const scenariosRouter = createTRPCRouter({
return { return {
scenarios, scenarios,
startIndex: (page - 1) * PAGE_SIZE + 1,
lastPage: Math.ceil(count / PAGE_SIZE),
count, count,
}; };
}), }),

View File

@@ -27,6 +27,7 @@ import { capturePath } from "~/utils/analytics/serverAnalytics";
type CreateContextOptions = { type CreateContextOptions = {
session: Session | null; session: Session | null;
apiKey: string | null;
}; };
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -45,6 +46,7 @@ const noOp = () => {};
export const createInnerTRPCContext = (opts: CreateContextOptions) => { export const createInnerTRPCContext = (opts: CreateContextOptions) => {
return { return {
session: opts.session, session: opts.session,
apiKey: opts.apiKey,
prisma, prisma,
markAccessControlRun: noOp, markAccessControlRun: noOp,
}; };
@@ -62,8 +64,11 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
// Get the session from the server using the getServerSession wrapper function // Get the session from the server using the getServerSession wrapper function
const session = await getServerAuthSession({ req, res }); const session = await getServerAuthSession({ req, res });
const apiKey = req.headers["x-openpipe-api-key"] as string | null;
return createInnerTRPCContext({ return createInnerTRPCContext({
session, session,
apiKey,
}); });
}; };

View File

@@ -1,41 +1,32 @@
import "dotenv/config"; import "dotenv/config";
import { openApiDocument } from "~/pages/api/v1/openapi.json"; import { openApiDocument } from "~/pages/api/openapi.json";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { execSync } from "child_process"; import { execSync } from "child_process";
import { generate } from "openapi-typescript-codegen";
console.log("Exporting public OpenAPI schema to client-libs/schema.json");
const scriptPath = import.meta.url.replace("file://", ""); const scriptPath = import.meta.url.replace("file://", "");
const clientLibsPath = path.join(path.dirname(scriptPath), "../../../../client-libs"); const clientLibsPath = path.join(path.dirname(scriptPath), "../../../../client-libs");
const schemaPath = path.join(clientLibsPath, "openapi.json"); const schemaPath = path.join(clientLibsPath, "schema.json");
console.log(`Exporting public OpenAPI schema to ${schemaPath}`);
console.log("Exporting schema");
fs.writeFileSync(schemaPath, JSON.stringify(openApiDocument, null, 2), "utf-8"); fs.writeFileSync(schemaPath, JSON.stringify(openApiDocument, null, 2), "utf-8");
console.log("Generating TypeScript client"); console.log("Generating Typescript client");
const tsClientPath = path.join(clientLibsPath, "typescript/src/codegen"); const tsClientPath = path.join(clientLibsPath, "typescript/codegen");
fs.rmSync(tsClientPath, { recursive: true, force: true }); fs.rmSync(tsClientPath, { recursive: true, force: true });
fs.mkdirSync(tsClientPath, { recursive: true });
await generate({ execSync(
input: openApiDocument, `pnpm dlx @openapitools/openapi-generator-cli generate -i "${schemaPath}" -g typescript-axios -o "${tsClientPath}"`,
output: tsClientPath, {
clientName: "OPClient", stdio: "inherit",
httpClient: "node", },
}); );
// execSync(
// `pnpm run openapi generate --input "${schemaPath}" --output "${tsClientPath}" --name OPClient --client node`,
// {
// stdio: "inherit",
// },
// );
console.log("Generating Python client");
execSync(path.join(clientLibsPath, "python/codegen.sh"));
console.log("Done!"); console.log("Done!");
process.exit(0);

View File

@@ -99,11 +99,11 @@ export const queryModel = defineTask<QueryModelJob>("queryModel", async (task) =
} }
: null; : null;
const cacheKey = hashObject(prompt as JsonValue); const inputHash = hashObject(prompt as JsonValue);
let modelResponse = await prisma.modelResponse.create({ let modelResponse = await prisma.modelResponse.create({
data: { data: {
cacheKey, inputHash,
scenarioVariantCellId: cellId, scenarioVariantCellId: cellId,
requestedAt: new Date(), requestedAt: new Date(),
}, },
@@ -114,7 +114,7 @@ export const queryModel = defineTask<QueryModelJob>("queryModel", async (task) =
modelResponse = await prisma.modelResponse.update({ modelResponse = await prisma.modelResponse.update({
where: { id: modelResponse.id }, where: { id: modelResponse.id },
data: { data: {
respPayload: response.value as Prisma.InputJsonObject, output: response.value as Prisma.InputJsonObject,
statusCode: response.statusCode, statusCode: response.statusCode,
receivedAt: new Date(), receivedAt: new Date(),
inputTokens: usage?.inputTokens, inputTokens: usage?.inputTokens,

View File

@@ -51,7 +51,7 @@ const requestUpdatedPromptFunction = async (
originalModelProvider.inputSchema, originalModelProvider.inputSchema,
null, null,
2, 2,
)}`, )}\n\nDo not add any assistant messages.`,
}, },
{ {
role: "user", role: "user",

View File

@@ -51,7 +51,7 @@ export const runAllEvals = async (experimentId: string) => {
const outputs = await prisma.modelResponse.findMany({ const outputs = await prisma.modelResponse.findMany({
where: { where: {
outdated: false, outdated: false,
respPayload: { output: {
not: Prisma.AnyNull, not: Prisma.AnyNull,
}, },
scenarioVariantCell: { scenarioVariantCell: {

View File

@@ -2,4 +2,4 @@ import cryptoRandomString from "crypto-random-string";
const KEY_LENGTH = 42; const KEY_LENGTH = 42;
export const generateApiKey = () => `opk_${cryptoRandomString({ length: KEY_LENGTH })}`; export const generateApiKey = () => `opc_${cryptoRandomString({ length: KEY_LENGTH })}`;

View File

@@ -57,7 +57,7 @@ export const generateNewCell = async (
return; return;
} }
const cacheKey = hashObject(parsedConstructFn); const inputHash = hashObject(parsedConstructFn);
cell = await prisma.scenarioVariantCell.create({ cell = await prisma.scenarioVariantCell.create({
data: { data: {
@@ -73,8 +73,8 @@ export const generateNewCell = async (
const matchingModelResponse = await prisma.modelResponse.findFirst({ const matchingModelResponse = await prisma.modelResponse.findFirst({
where: { where: {
cacheKey, inputHash,
respPayload: { output: {
not: Prisma.AnyNull, not: Prisma.AnyNull,
}, },
}, },
@@ -92,7 +92,7 @@ export const generateNewCell = async (
data: { data: {
...omit(matchingModelResponse, ["id", "scenarioVariantCell"]), ...omit(matchingModelResponse, ["id", "scenarioVariantCell"]),
scenarioVariantCellId: cell.id, scenarioVariantCellId: cell.id,
respPayload: matchingModelResponse.respPayload as Prisma.InputJsonValue, output: matchingModelResponse.output as Prisma.InputJsonValue,
}, },
}); });

View File

@@ -1,29 +1,13 @@
import fs from "fs";
import path from "path";
import OpenAI, { type ClientOptions } from "openpipe/src/openai";
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
let config: ClientOptions; import { default as OriginalOpenAI } from "openai";
// import { OpenAI } from "openpipe";
try { const openAIConfig = { apiKey: env.OPENAI_API_KEY ?? "dummy-key" };
// Allow developers to override the config with a local file
const jsonData = fs.readFileSync(
path.join(path.dirname(import.meta.url).replace("file://", ""), "./openaiCustomConfig.json"),
"utf8",
);
config = JSON.parse(jsonData.toString());
} catch (error) {
// Set a dummy key so it doesn't fail at build time
config = {
apiKey: env.OPENAI_API_KEY ?? "dummy-key",
openpipe: {
apiKey: env.OPENPIPE_API_KEY,
baseUrl: "http://localhost:3000/api/v1",
},
};
}
// export const openai = env.OPENPIPE_API_KEY ? new OpenAI.OpenAI(config) : new OriginalOpenAI(config); // Set a dummy key so it doesn't fail at build time
// export const openai = env.OPENPIPE_API_KEY
// ? new OpenAI.OpenAI(openAIConfig)
// : new OriginalOpenAI(openAIConfig);
export const openai = new OpenAI(config); export const openai = new OriginalOpenAI(openAIConfig);

View File

@@ -71,7 +71,7 @@ export const runOneEval = async (
provider: SupportedProvider, provider: SupportedProvider,
): Promise<{ result: number; details?: string }> => { ): Promise<{ result: number; details?: string }> => {
const modelProvider = modelProviders[provider]; const modelProvider = modelProviders[provider];
const message = modelProvider.normalizeOutput(modelResponse.respPayload); const message = modelProvider.normalizeOutput(modelResponse.output);
if (!message) return { result: 0 }; if (!message) return { result: 0 };

View File

@@ -1,30 +0,0 @@
import { type SliceCreator } from "./store";
export const editorBackground = "#fafafa";
export type SelectedLogsSlice = {
selectedLogIds: Set<string>;
toggleSelectedLogId: (id: string) => void;
addSelectedLogIds: (ids: string[]) => void;
clearSelectedLogIds: () => void;
};
export const createSelectedLogsSlice: SliceCreator<SelectedLogsSlice> = (set, get) => ({
selectedLogIds: new Set(),
toggleSelectedLogId: (id: string) =>
set((state) => {
if (state.selectedLogs.selectedLogIds.has(id)) {
state.selectedLogs.selectedLogIds.delete(id);
} else {
state.selectedLogs.selectedLogIds.add(id);
}
}),
addSelectedLogIds: (ids: string[]) =>
set((state) => {
state.selectedLogs.selectedLogIds = new Set([...state.selectedLogs.selectedLogIds, ...ids]);
}),
clearSelectedLogIds: () =>
set((state) => {
state.selectedLogs.selectedLogIds = new Set();
}),
});

View File

@@ -81,6 +81,8 @@ export const createVariantEditorSlice: SliceCreator<SharedVariantEditorSlice> =
}; };
`; `;
console.log(modelContents);
const scenariosModel = monaco.editor.getModel(monaco.Uri.parse("file:///scenarios.ts")); const scenariosModel = monaco.editor.getModel(monaco.Uri.parse("file:///scenarios.ts"));
if (scenariosModel) { if (scenariosModel) {

View File

@@ -1,6 +1,5 @@
import { type StateCreator, create } from "zustand"; import { type StateCreator, create } from "zustand";
import { immer } from "zustand/middleware/immer"; import { immer } from "zustand/middleware/immer";
import { enableMapSet } from "immer";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { createSelectors } from "./createSelectors"; import { createSelectors } from "./createSelectors";
import { import {
@@ -9,9 +8,6 @@ import {
} from "./sharedVariantEditor.slice"; } from "./sharedVariantEditor.slice";
import { type APIClient } from "~/utils/api"; import { type APIClient } from "~/utils/api";
import { persistOptions, type stateToPersist } from "./persist"; import { persistOptions, type stateToPersist } from "./persist";
import { type SelectedLogsSlice, createSelectedLogsSlice } from "./selectedLogsSlice";
enableMapSet();
export type State = { export type State = {
drawerOpen: boolean; drawerOpen: boolean;
@@ -21,8 +17,7 @@ export type State = {
setApi: (api: APIClient) => void; setApi: (api: APIClient) => void;
sharedVariantEditor: SharedVariantEditorSlice; sharedVariantEditor: SharedVariantEditorSlice;
selectedProjectId: string | null; selectedProjectId: string | null;
setSelectedProjectId: (id: string) => void; setselectedProjectId: (id: string) => void;
selectedLogs: SelectedLogsSlice;
}; };
export type SliceCreator<T> = StateCreator<State, [["zustand/immer", never]], [], T>; export type SliceCreator<T> = StateCreator<State, [["zustand/immer", never]], [], T>;
@@ -53,11 +48,10 @@ const useBaseStore = create<
}), }),
sharedVariantEditor: createVariantEditorSlice(set, get, ...rest), sharedVariantEditor: createVariantEditorSlice(set, get, ...rest),
selectedProjectId: null, selectedProjectId: null,
setSelectedProjectId: (id: string) => setselectedProjectId: (id: string) =>
set((state) => { set((state) => {
state.selectedProjectId = id; state.selectedProjectId = id;
}), }),
selectedLogs: createSelectedLogsSlice(set, get, ...rest),
})), })),
persistOptions, persistOptions,
), ),

View File

@@ -0,0 +1,31 @@
import { type Session } from "next-auth";
import { useSession } from "next-auth/react";
import { useEffect } from "react";
import posthog from "posthog-js";
import { env } from "~/env.mjs";
// Make sure we're in the browser
const enableBrowserAnalytics = typeof window !== "undefined";
if (env.NEXT_PUBLIC_POSTHOG_KEY && enableBrowserAnalytics) {
posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: `${env.NEXT_PUBLIC_HOST}/ingest`,
});
}
export const identifySession = (session: Session) => {
if (!session.user) return;
posthog.identify(session.user.id, {
name: session.user.name,
email: session.user.email,
});
};
export const SessionIdentifier = () => {
const session = useSession().data;
useEffect(() => {
if (session && enableBrowserAnalytics) identifySession(session);
}, [session]);
return null;
};

View File

@@ -1,41 +0,0 @@
"use client";
import { useSession } from "next-auth/react";
import React, { type ReactNode, useEffect } from "react";
import { PostHogProvider } from "posthog-js/react";
import posthog from "posthog-js";
import { env } from "~/env.mjs";
import { useRouter } from "next/router";
// Make sure we're in the browser
const inBrowser = typeof window !== "undefined";
export const PosthogAppProvider = ({ children }: { children: ReactNode }) => {
const session = useSession().data;
const router = useRouter();
useEffect(() => {
// Track page views
const handleRouteChange = () => posthog?.capture("$pageview");
router.events.on("routeChangeComplete", handleRouteChange);
return () => {
router.events.off("routeChangeComplete", handleRouteChange);
};
}, [router.events]);
useEffect(() => {
if (env.NEXT_PUBLIC_POSTHOG_KEY && inBrowser && session && session.user) {
posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: `${env.NEXT_PUBLIC_HOST}/ingest`,
});
posthog.identify(session.user.id, {
name: session.user.name,
email: session.user.email,
});
}
}, [session]);
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
};

View File

@@ -1,11 +0,0 @@
export function error(message: string): { status: "error"; message: string } {
return {
status: "error",
message,
};
}
export function success<T>(payload: T): { status: "success"; payload: T };
export function success(payload?: undefined): { status: "success"; payload: undefined };
export function success<T>(payload?: T) {
return { status: "success", payload };
}

View File

@@ -1,7 +1,7 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { type RefObject, useCallback, useEffect, useRef, useState } from "react"; import { type RefObject, useCallback, useEffect, useRef, useState } from "react";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { NumberParam, useQueryParams } from "use-query-params"; import { NumberParam, useQueryParam, withDefault } from "use-query-params";
import { useAppStore } from "~/state/store"; import { useAppStore } from "~/state/store";
export const useExperiments = () => { export const useExperiments = () => {
@@ -46,10 +46,10 @@ export const useDataset = () => {
export const useDatasetEntries = () => { export const useDatasetEntries = () => {
const dataset = useDataset(); const dataset = useDataset();
const { page, pageSize } = usePageParams(); const [page] = usePage();
return api.datasetEntries.list.useQuery( return api.datasetEntries.list.useQuery(
{ datasetId: dataset.data?.id ?? "", page, pageSize }, { datasetId: dataset.data?.id ?? "", page },
{ enabled: dataset.data?.id != null }, { enabled: dataset.data?.id != null },
); );
}; };
@@ -132,23 +132,14 @@ export const useElementDimensions = (): [RefObject<HTMLElement>, Dimensions | un
return [ref, dimensions]; return [ref, dimensions];
}; };
export const usePageParams = () => { export const usePage = () => useQueryParam("page", withDefault(NumberParam, 1));
const [pageParams, setPageParams] = useQueryParams({
page: NumberParam,
pageSize: NumberParam,
});
const { page, pageSize } = pageParams;
return { page: page || 1, pageSize: pageSize || 10, setPageParams };
};
export const useScenarios = () => { export const useScenarios = () => {
const experiment = useExperiment(); const experiment = useExperiment();
const { page, pageSize } = usePageParams(); const [page] = usePage();
return api.scenarios.list.useQuery( return api.scenarios.list.useQuery(
{ experimentId: experiment.data?.id ?? "", page, pageSize }, { experimentId: experiment.data?.id ?? "", page },
{ enabled: experiment.data?.id != null }, { enabled: experiment.data?.id != null },
); );
}; };
@@ -175,13 +166,3 @@ export const useScenarioVars = () => {
{ enabled: experiment.data?.id != null }, { enabled: experiment.data?.id != null },
); );
}; };
export const useLoggedCalls = () => {
const selectedProjectId = useAppStore((state) => state.selectedProjectId);
const { page, pageSize } = usePageParams();
return api.loggedCalls.list.useQuery(
{ projectId: selectedProjectId ?? "", page, pageSize },
{ enabled: !!selectedProjectId },
);
};

View File

@@ -1,5 +1,16 @@
import { toast } from "~/theme/ChakraThemeProvider"; import { toast } from "~/theme/ChakraThemeProvider";
import { type error, type success } from "./standardResponses";
export function error(message: string): { status: "error"; message: string } {
return {
status: "error",
message,
};
}
export function success<T>(payload: T): { status: "success"; payload: T };
export function success(payload?: undefined): { status: "success"; payload: undefined };
export function success<T>(payload?: T) {
return { status: "success", payload };
}
type SuccessType<T> = ReturnType<typeof success<T>>; type SuccessType<T> = ReturnType<typeof success<T>>;
type ErrorType = ReturnType<typeof error>; type ErrorType = ReturnType<typeof error>;

View File

@@ -1,9 +0,0 @@
#! /bin/bash
set -e
cd "$(dirname "$0")/.."
source app/.env
docker build . --file app/Dockerfile

View File

@@ -25,11 +25,11 @@
".eslintrc.cjs", ".eslintrc.cjs",
"next-env.d.ts", "next-env.d.ts",
"**/*.ts", "**/*.ts",
"**/*.mts",
"**/*.tsx", "**/*.tsx",
"**/*.cjs", "**/*.cjs",
"**/*.mjs", "**/*.mjs",
"**/*.js" "**/*.js",
"src/pages/api/sentry-example-api.js"
], ],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

Binary file not shown.

Binary file not shown.

View File

@@ -1,11 +0,0 @@
#! /bin/bash
set -e
cd "$(dirname "$0")"
poetry run openapi-python-client generate --path ../openapi.json
rm -rf openpipe/api_client
mv open-pipe-api-client/open_pipe_api_client openpipe/api_client
rm -rf open-pipe-api-client

View File

@@ -1,10 +0,0 @@
from .openai import OpenAIWrapper
from .shared import configured_client
openai = OpenAIWrapper()
def configure_openpipe(base_url=None, api_key=None):
if base_url is not None:
configured_client._base_url = base_url
if api_key is not None:
configured_client.token = api_key

View File

@@ -1,7 +0,0 @@
""" A client library for accessing OpenPipe API """
from .client import AuthenticatedClient, Client
__all__ = (
"AuthenticatedClient",
"Client",
)

View File

@@ -1 +0,0 @@
""" Contains methods for accessing the API """

View File

@@ -1,155 +0,0 @@
from http import HTTPStatus
from typing import Any, Dict, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.check_cache_json_body import CheckCacheJsonBody
from ...models.check_cache_response_200 import CheckCacheResponse200
from ...types import Response
def _get_kwargs(
*,
json_body: CheckCacheJsonBody,
) -> Dict[str, Any]:
pass
json_json_body = json_body.to_dict()
return {
"method": "post",
"url": "/check-cache",
"json": json_json_body,
}
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[CheckCacheResponse200]:
if response.status_code == HTTPStatus.OK:
response_200 = CheckCacheResponse200.from_dict(response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[CheckCacheResponse200]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient,
json_body: CheckCacheJsonBody,
) -> Response[CheckCacheResponse200]:
"""Check if a prompt is cached
Args:
json_body (CheckCacheJsonBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[CheckCacheResponse200]
"""
kwargs = _get_kwargs(
json_body=json_body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient,
json_body: CheckCacheJsonBody,
) -> Optional[CheckCacheResponse200]:
"""Check if a prompt is cached
Args:
json_body (CheckCacheJsonBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
CheckCacheResponse200
"""
return sync_detailed(
client=client,
json_body=json_body,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient,
json_body: CheckCacheJsonBody,
) -> Response[CheckCacheResponse200]:
"""Check if a prompt is cached
Args:
json_body (CheckCacheJsonBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[CheckCacheResponse200]
"""
kwargs = _get_kwargs(
json_body=json_body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient,
json_body: CheckCacheJsonBody,
) -> Optional[CheckCacheResponse200]:
"""Check if a prompt is cached
Args:
json_body (CheckCacheJsonBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
CheckCacheResponse200
"""
return (
await asyncio_detailed(
client=client,
json_body=json_body,
)
).parsed

View File

@@ -1,133 +0,0 @@
from http import HTTPStatus
from typing import Any, Dict, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.local_testing_only_get_latest_logged_call_response_200 import (
LocalTestingOnlyGetLatestLoggedCallResponse200,
)
from ...types import Response
def _get_kwargs() -> Dict[str, Any]:
pass
return {
"method": "get",
"url": "/local-testing-only-get-latest-logged-call",
}
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]:
if response.status_code == HTTPStatus.OK:
_response_200 = response.json()
response_200: Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]
if _response_200 is None:
response_200 = None
else:
response_200 = LocalTestingOnlyGetLatestLoggedCallResponse200.from_dict(_response_200)
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient,
) -> Response[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]:
"""Get the latest logged call (only for local testing)
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]
"""
kwargs = _get_kwargs()
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient,
) -> Optional[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]:
"""Get the latest logged call (only for local testing)
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]
"""
return sync_detailed(
client=client,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient,
) -> Response[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]:
"""Get the latest logged call (only for local testing)
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]
"""
kwargs = _get_kwargs()
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient,
) -> Optional[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]:
"""Get the latest logged call (only for local testing)
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]
"""
return (
await asyncio_detailed(
client=client,
)
).parsed

View File

@@ -1,155 +0,0 @@
from http import HTTPStatus
from typing import Any, Dict, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.report_json_body import ReportJsonBody
from ...models.report_response_200 import ReportResponse200
from ...types import Response
def _get_kwargs(
*,
json_body: ReportJsonBody,
) -> Dict[str, Any]:
pass
json_json_body = json_body.to_dict()
return {
"method": "post",
"url": "/report",
"json": json_json_body,
}
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[ReportResponse200]:
if response.status_code == HTTPStatus.OK:
response_200 = ReportResponse200.from_dict(response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[ReportResponse200]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient,
json_body: ReportJsonBody,
) -> Response[ReportResponse200]:
"""Report an API call
Args:
json_body (ReportJsonBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[ReportResponse200]
"""
kwargs = _get_kwargs(
json_body=json_body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient,
json_body: ReportJsonBody,
) -> Optional[ReportResponse200]:
"""Report an API call
Args:
json_body (ReportJsonBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
ReportResponse200
"""
return sync_detailed(
client=client,
json_body=json_body,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient,
json_body: ReportJsonBody,
) -> Response[ReportResponse200]:
"""Report an API call
Args:
json_body (ReportJsonBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[ReportResponse200]
"""
kwargs = _get_kwargs(
json_body=json_body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient,
json_body: ReportJsonBody,
) -> Optional[ReportResponse200]:
"""Report an API call
Args:
json_body (ReportJsonBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
ReportResponse200
"""
return (
await asyncio_detailed(
client=client,
json_body=json_body,
)
).parsed

View File

@@ -1,268 +0,0 @@
import ssl
from typing import Any, Dict, Optional, Union
import httpx
from attrs import define, evolve, field
@define
class Client:
"""A class for keeping track of data related to the API
The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
``base_url``: The base URL for the API, all requests are made to a relative path to this URL
``cookies``: A dictionary of cookies to be sent with every request
``headers``: A dictionary of headers to be sent with every request
``timeout``: The maximum amount of a time a request can take. API functions will raise
httpx.TimeoutException if this is exceeded.
``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
but can be set to False for testing purposes.
``follow_redirects``: Whether or not to follow redirects. Default value is False.
``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
Attributes:
raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
argument to the constructor.
"""
raise_on_unexpected_status: bool = field(default=False, kw_only=True)
_base_url: str
_cookies: Dict[str, str] = field(factory=dict, kw_only=True)
_headers: Dict[str, str] = field(factory=dict, kw_only=True)
_timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True)
_verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True)
_follow_redirects: bool = field(default=False, kw_only=True)
_httpx_args: Dict[str, Any] = field(factory=dict, kw_only=True)
_client: Optional[httpx.Client] = field(default=None, init=False)
_async_client: Optional[httpx.AsyncClient] = field(default=None, init=False)
def with_headers(self, headers: Dict[str, str]) -> "Client":
"""Get a new client matching this one with additional headers"""
if self._client is not None:
self._client.headers.update(headers)
if self._async_client is not None:
self._async_client.headers.update(headers)
return evolve(self, headers={**self._headers, **headers})
def with_cookies(self, cookies: Dict[str, str]) -> "Client":
"""Get a new client matching this one with additional cookies"""
if self._client is not None:
self._client.cookies.update(cookies)
if self._async_client is not None:
self._async_client.cookies.update(cookies)
return evolve(self, cookies={**self._cookies, **cookies})
def with_timeout(self, timeout: httpx.Timeout) -> "Client":
"""Get a new client matching this one with a new timeout (in seconds)"""
if self._client is not None:
self._client.timeout = timeout
if self._async_client is not None:
self._async_client.timeout = timeout
return evolve(self, timeout=timeout)
def set_httpx_client(self, client: httpx.Client) -> "Client":
"""Manually the underlying httpx.Client
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
"""
self._client = client
return self
def get_httpx_client(self) -> httpx.Client:
"""Get the underlying httpx.Client, constructing a new one if not previously set"""
if self._client is None:
self._client = httpx.Client(
base_url=self._base_url,
cookies=self._cookies,
headers=self._headers,
timeout=self._timeout,
verify=self._verify_ssl,
follow_redirects=self._follow_redirects,
**self._httpx_args,
)
return self._client
def __enter__(self) -> "Client":
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
self.get_httpx_client().__enter__()
return self
def __exit__(self, *args: Any, **kwargs: Any) -> None:
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
self.get_httpx_client().__exit__(*args, **kwargs)
def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client":
"""Manually the underlying httpx.AsyncClient
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
"""
self._async_client = async_client
return self
def get_async_httpx_client(self) -> httpx.AsyncClient:
"""Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
if self._async_client is None:
self._async_client = httpx.AsyncClient(
base_url=self._base_url,
cookies=self._cookies,
headers=self._headers,
timeout=self._timeout,
verify=self._verify_ssl,
follow_redirects=self._follow_redirects,
**self._httpx_args,
)
return self._async_client
async def __aenter__(self) -> "Client":
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
await self.get_async_httpx_client().__aenter__()
return self
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
"""Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
await self.get_async_httpx_client().__aexit__(*args, **kwargs)
@define
class AuthenticatedClient:
"""A Client which has been authenticated for use on secured endpoints
The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
``base_url``: The base URL for the API, all requests are made to a relative path to this URL
``cookies``: A dictionary of cookies to be sent with every request
``headers``: A dictionary of headers to be sent with every request
``timeout``: The maximum amount of a time a request can take. API functions will raise
httpx.TimeoutException if this is exceeded.
``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
but can be set to False for testing purposes.
``follow_redirects``: Whether or not to follow redirects. Default value is False.
``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
Attributes:
raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
argument to the constructor.
token: The token to use for authentication
prefix: The prefix to use for the Authorization header
auth_header_name: The name of the Authorization header
"""
raise_on_unexpected_status: bool = field(default=False, kw_only=True)
_base_url: str
_cookies: Dict[str, str] = field(factory=dict, kw_only=True)
_headers: Dict[str, str] = field(factory=dict, kw_only=True)
_timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True)
_verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True)
_follow_redirects: bool = field(default=False, kw_only=True)
_httpx_args: Dict[str, Any] = field(factory=dict, kw_only=True)
_client: Optional[httpx.Client] = field(default=None, init=False)
_async_client: Optional[httpx.AsyncClient] = field(default=None, init=False)
token: str
prefix: str = "Bearer"
auth_header_name: str = "Authorization"
def with_headers(self, headers: Dict[str, str]) -> "AuthenticatedClient":
"""Get a new client matching this one with additional headers"""
if self._client is not None:
self._client.headers.update(headers)
if self._async_client is not None:
self._async_client.headers.update(headers)
return evolve(self, headers={**self._headers, **headers})
def with_cookies(self, cookies: Dict[str, str]) -> "AuthenticatedClient":
"""Get a new client matching this one with additional cookies"""
if self._client is not None:
self._client.cookies.update(cookies)
if self._async_client is not None:
self._async_client.cookies.update(cookies)
return evolve(self, cookies={**self._cookies, **cookies})
def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient":
"""Get a new client matching this one with a new timeout (in seconds)"""
if self._client is not None:
self._client.timeout = timeout
if self._async_client is not None:
self._async_client.timeout = timeout
return evolve(self, timeout=timeout)
def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient":
"""Manually the underlying httpx.Client
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
"""
self._client = client
return self
def get_httpx_client(self) -> httpx.Client:
"""Get the underlying httpx.Client, constructing a new one if not previously set"""
if self._client is None:
self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
self._client = httpx.Client(
base_url=self._base_url,
cookies=self._cookies,
headers=self._headers,
timeout=self._timeout,
verify=self._verify_ssl,
follow_redirects=self._follow_redirects,
**self._httpx_args,
)
return self._client
def __enter__(self) -> "AuthenticatedClient":
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
self.get_httpx_client().__enter__()
return self
def __exit__(self, *args: Any, **kwargs: Any) -> None:
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
self.get_httpx_client().__exit__(*args, **kwargs)
def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient":
"""Manually the underlying httpx.AsyncClient
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
"""
self._async_client = async_client
return self
def get_async_httpx_client(self) -> httpx.AsyncClient:
"""Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
if self._async_client is None:
self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
self._async_client = httpx.AsyncClient(
base_url=self._base_url,
cookies=self._cookies,
headers=self._headers,
timeout=self._timeout,
verify=self._verify_ssl,
follow_redirects=self._follow_redirects,
**self._httpx_args,
)
return self._async_client
async def __aenter__(self) -> "AuthenticatedClient":
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
await self.get_async_httpx_client().__aenter__()
return self
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
"""Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
await self.get_async_httpx_client().__aexit__(*args, **kwargs)

View File

@@ -1,14 +0,0 @@
""" Contains shared errors types that can be raised from API functions """
class UnexpectedStatus(Exception):
"""Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True"""
def __init__(self, status_code: int, content: bytes):
self.status_code = status_code
self.content = content
super().__init__(f"Unexpected status code: {status_code}")
__all__ = ["UnexpectedStatus"]

View File

@@ -1,29 +0,0 @@
""" Contains all the data models used in inputs/outputs """
from .check_cache_json_body import CheckCacheJsonBody
from .check_cache_json_body_tags import CheckCacheJsonBodyTags
from .check_cache_response_200 import CheckCacheResponse200
from .local_testing_only_get_latest_logged_call_response_200 import LocalTestingOnlyGetLatestLoggedCallResponse200
from .local_testing_only_get_latest_logged_call_response_200_model_response import (
LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse,
)
from .local_testing_only_get_latest_logged_call_response_200_tags import (
LocalTestingOnlyGetLatestLoggedCallResponse200Tags,
)
from .report_json_body import ReportJsonBody
from .report_json_body_tags import ReportJsonBodyTags
from .report_response_200 import ReportResponse200
from .report_response_200_status import ReportResponse200Status
__all__ = (
"CheckCacheJsonBody",
"CheckCacheJsonBodyTags",
"CheckCacheResponse200",
"LocalTestingOnlyGetLatestLoggedCallResponse200",
"LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse",
"LocalTestingOnlyGetLatestLoggedCallResponse200Tags",
"ReportJsonBody",
"ReportJsonBodyTags",
"ReportResponse200",
"ReportResponse200Status",
)

View File

@@ -1,70 +0,0 @@
from typing import TYPE_CHECKING, Any, Dict, Type, TypeVar, Union
from attrs import define
from ..types import UNSET, Unset
if TYPE_CHECKING:
from ..models.check_cache_json_body_tags import CheckCacheJsonBodyTags
T = TypeVar("T", bound="CheckCacheJsonBody")
@define
class CheckCacheJsonBody:
"""
Attributes:
requested_at (float): Unix timestamp in milliseconds
req_payload (Union[Unset, Any]): JSON-encoded request payload
tags (Union[Unset, CheckCacheJsonBodyTags]): Extra tags to attach to the call for filtering. Eg { "userId":
"123", "promptId": "populate-title" }
"""
requested_at: float
req_payload: Union[Unset, Any] = UNSET
tags: Union[Unset, "CheckCacheJsonBodyTags"] = UNSET
def to_dict(self) -> Dict[str, Any]:
requested_at = self.requested_at
req_payload = self.req_payload
tags: Union[Unset, Dict[str, Any]] = UNSET
if not isinstance(self.tags, Unset):
tags = self.tags.to_dict()
field_dict: Dict[str, Any] = {}
field_dict.update(
{
"requestedAt": requested_at,
}
)
if req_payload is not UNSET:
field_dict["reqPayload"] = req_payload
if tags is not UNSET:
field_dict["tags"] = tags
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
from ..models.check_cache_json_body_tags import CheckCacheJsonBodyTags
d = src_dict.copy()
requested_at = d.pop("requestedAt")
req_payload = d.pop("reqPayload", UNSET)
_tags = d.pop("tags", UNSET)
tags: Union[Unset, CheckCacheJsonBodyTags]
if isinstance(_tags, Unset):
tags = UNSET
else:
tags = CheckCacheJsonBodyTags.from_dict(_tags)
check_cache_json_body = cls(
requested_at=requested_at,
req_payload=req_payload,
tags=tags,
)
return check_cache_json_body

View File

@@ -1,43 +0,0 @@
from typing import Any, Dict, List, Type, TypeVar
from attrs import define, field
T = TypeVar("T", bound="CheckCacheJsonBodyTags")
@define
class CheckCacheJsonBodyTags:
"""Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }"""
additional_properties: Dict[str, str] = field(init=False, factory=dict)
def to_dict(self) -> Dict[str, Any]:
field_dict: Dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update({})
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
d = src_dict.copy()
check_cache_json_body_tags = cls()
check_cache_json_body_tags.additional_properties = d
return check_cache_json_body_tags
@property
def additional_keys(self) -> List[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> str:
return self.additional_properties[key]
def __setitem__(self, key: str, value: str) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View File

@@ -1,38 +0,0 @@
from typing import Any, Dict, Type, TypeVar, Union
from attrs import define
from ..types import UNSET, Unset
T = TypeVar("T", bound="CheckCacheResponse200")
@define
class CheckCacheResponse200:
"""
Attributes:
resp_payload (Union[Unset, Any]): JSON-encoded response payload
"""
resp_payload: Union[Unset, Any] = UNSET
def to_dict(self) -> Dict[str, Any]:
resp_payload = self.resp_payload
field_dict: Dict[str, Any] = {}
field_dict.update({})
if resp_payload is not UNSET:
field_dict["respPayload"] = resp_payload
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
d = src_dict.copy()
resp_payload = d.pop("respPayload", UNSET)
check_cache_response_200 = cls(
resp_payload=resp_payload,
)
return check_cache_response_200

View File

@@ -1,84 +0,0 @@
import datetime
from typing import TYPE_CHECKING, Any, Dict, Optional, Type, TypeVar
from attrs import define
from dateutil.parser import isoparse
if TYPE_CHECKING:
from ..models.local_testing_only_get_latest_logged_call_response_200_model_response import (
LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse,
)
from ..models.local_testing_only_get_latest_logged_call_response_200_tags import (
LocalTestingOnlyGetLatestLoggedCallResponse200Tags,
)
T = TypeVar("T", bound="LocalTestingOnlyGetLatestLoggedCallResponse200")
@define
class LocalTestingOnlyGetLatestLoggedCallResponse200:
"""
Attributes:
created_at (datetime.datetime):
cache_hit (bool):
tags (LocalTestingOnlyGetLatestLoggedCallResponse200Tags):
model_response (Optional[LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse]):
"""
created_at: datetime.datetime
cache_hit: bool
tags: "LocalTestingOnlyGetLatestLoggedCallResponse200Tags"
model_response: Optional["LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse"]
def to_dict(self) -> Dict[str, Any]:
created_at = self.created_at.isoformat()
cache_hit = self.cache_hit
tags = self.tags.to_dict()
model_response = self.model_response.to_dict() if self.model_response else None
field_dict: Dict[str, Any] = {}
field_dict.update(
{
"createdAt": created_at,
"cacheHit": cache_hit,
"tags": tags,
"modelResponse": model_response,
}
)
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
from ..models.local_testing_only_get_latest_logged_call_response_200_model_response import (
LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse,
)
from ..models.local_testing_only_get_latest_logged_call_response_200_tags import (
LocalTestingOnlyGetLatestLoggedCallResponse200Tags,
)
d = src_dict.copy()
created_at = isoparse(d.pop("createdAt"))
cache_hit = d.pop("cacheHit")
tags = LocalTestingOnlyGetLatestLoggedCallResponse200Tags.from_dict(d.pop("tags"))
_model_response = d.pop("modelResponse")
model_response: Optional[LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse]
if _model_response is None:
model_response = None
else:
model_response = LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse.from_dict(_model_response)
local_testing_only_get_latest_logged_call_response_200 = cls(
created_at=created_at,
cache_hit=cache_hit,
tags=tags,
model_response=model_response,
)
return local_testing_only_get_latest_logged_call_response_200

View File

@@ -1,70 +0,0 @@
from typing import Any, Dict, Optional, Type, TypeVar, Union
from attrs import define
from ..types import UNSET, Unset
T = TypeVar("T", bound="LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse")
@define
class LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse:
"""
Attributes:
id (str):
status_code (Optional[float]):
error_message (Optional[str]):
req_payload (Union[Unset, Any]):
resp_payload (Union[Unset, Any]):
"""
id: str
status_code: Optional[float]
error_message: Optional[str]
req_payload: Union[Unset, Any] = UNSET
resp_payload: Union[Unset, Any] = UNSET
def to_dict(self) -> Dict[str, Any]:
id = self.id
status_code = self.status_code
error_message = self.error_message
req_payload = self.req_payload
resp_payload = self.resp_payload
field_dict: Dict[str, Any] = {}
field_dict.update(
{
"id": id,
"statusCode": status_code,
"errorMessage": error_message,
}
)
if req_payload is not UNSET:
field_dict["reqPayload"] = req_payload
if resp_payload is not UNSET:
field_dict["respPayload"] = resp_payload
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
d = src_dict.copy()
id = d.pop("id")
status_code = d.pop("statusCode")
error_message = d.pop("errorMessage")
req_payload = d.pop("reqPayload", UNSET)
resp_payload = d.pop("respPayload", UNSET)
local_testing_only_get_latest_logged_call_response_200_model_response = cls(
id=id,
status_code=status_code,
error_message=error_message,
req_payload=req_payload,
resp_payload=resp_payload,
)
return local_testing_only_get_latest_logged_call_response_200_model_response

View File

@@ -1,43 +0,0 @@
from typing import Any, Dict, List, Optional, Type, TypeVar
from attrs import define, field
T = TypeVar("T", bound="LocalTestingOnlyGetLatestLoggedCallResponse200Tags")
@define
class LocalTestingOnlyGetLatestLoggedCallResponse200Tags:
""" """
additional_properties: Dict[str, Optional[str]] = field(init=False, factory=dict)
def to_dict(self) -> Dict[str, Any]:
field_dict: Dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update({})
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
d = src_dict.copy()
local_testing_only_get_latest_logged_call_response_200_tags = cls()
local_testing_only_get_latest_logged_call_response_200_tags.additional_properties = d
return local_testing_only_get_latest_logged_call_response_200_tags
@property
def additional_keys(self) -> List[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Optional[str]:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Optional[str]) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View File

@@ -1,101 +0,0 @@
from typing import TYPE_CHECKING, Any, Dict, Type, TypeVar, Union
from attrs import define
from ..types import UNSET, Unset
if TYPE_CHECKING:
from ..models.report_json_body_tags import ReportJsonBodyTags
T = TypeVar("T", bound="ReportJsonBody")
@define
class ReportJsonBody:
"""
Attributes:
requested_at (float): Unix timestamp in milliseconds
received_at (float): Unix timestamp in milliseconds
req_payload (Union[Unset, Any]): JSON-encoded request payload
resp_payload (Union[Unset, Any]): JSON-encoded response payload
status_code (Union[Unset, float]): HTTP status code of response
error_message (Union[Unset, str]): User-friendly error message
tags (Union[Unset, ReportJsonBodyTags]): Extra tags to attach to the call for filtering. Eg { "userId": "123",
"promptId": "populate-title" }
"""
requested_at: float
received_at: float
req_payload: Union[Unset, Any] = UNSET
resp_payload: Union[Unset, Any] = UNSET
status_code: Union[Unset, float] = UNSET
error_message: Union[Unset, str] = UNSET
tags: Union[Unset, "ReportJsonBodyTags"] = UNSET
def to_dict(self) -> Dict[str, Any]:
requested_at = self.requested_at
received_at = self.received_at
req_payload = self.req_payload
resp_payload = self.resp_payload
status_code = self.status_code
error_message = self.error_message
tags: Union[Unset, Dict[str, Any]] = UNSET
if not isinstance(self.tags, Unset):
tags = self.tags.to_dict()
field_dict: Dict[str, Any] = {}
field_dict.update(
{
"requestedAt": requested_at,
"receivedAt": received_at,
}
)
if req_payload is not UNSET:
field_dict["reqPayload"] = req_payload
if resp_payload is not UNSET:
field_dict["respPayload"] = resp_payload
if status_code is not UNSET:
field_dict["statusCode"] = status_code
if error_message is not UNSET:
field_dict["errorMessage"] = error_message
if tags is not UNSET:
field_dict["tags"] = tags
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
from ..models.report_json_body_tags import ReportJsonBodyTags
d = src_dict.copy()
requested_at = d.pop("requestedAt")
received_at = d.pop("receivedAt")
req_payload = d.pop("reqPayload", UNSET)
resp_payload = d.pop("respPayload", UNSET)
status_code = d.pop("statusCode", UNSET)
error_message = d.pop("errorMessage", UNSET)
_tags = d.pop("tags", UNSET)
tags: Union[Unset, ReportJsonBodyTags]
if isinstance(_tags, Unset):
tags = UNSET
else:
tags = ReportJsonBodyTags.from_dict(_tags)
report_json_body = cls(
requested_at=requested_at,
received_at=received_at,
req_payload=req_payload,
resp_payload=resp_payload,
status_code=status_code,
error_message=error_message,
tags=tags,
)
return report_json_body

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