Compare commits
5 Commits
fix-build
...
project-me
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e176088e9 | ||
|
|
3cec1f7786 | ||
|
|
b3d8f96fa8 | ||
|
|
54d97ddfa8 | ||
|
|
1f8e3b820f |
@@ -1,5 +0,0 @@
|
|||||||
**/node_modules/
|
|
||||||
.git
|
|
||||||
**/.venv/
|
|
||||||
**/.env*
|
|
||||||
**/.next/
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +0,0 @@
|
|||||||
.env
|
|
||||||
.venv/
|
|
||||||
*.pyc
|
|
||||||
node_modules/
|
|
||||||
*.tsbuildinfo
|
|
||||||
@@ -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
3
app/.gitignore
vendored
@@ -44,6 +44,3 @@ yarn-error.log*
|
|||||||
|
|
||||||
# Sentry Auth Token
|
# Sentry Auth Token
|
||||||
.sentryclirc
|
.sentryclirc
|
||||||
|
|
||||||
# custom openai intialization
|
|
||||||
src/server/utils/openaiCustomConfig.json
|
|
||||||
|
|||||||
3
app/@types/nextjs-routes.d.ts
vendored
3
app/@types/nextjs-routes.d.ts
vendored
@@ -18,14 +18,13 @@ declare module "nextjs-routes" {
|
|||||||
| StaticRoute<"/api/openapi">
|
| StaticRoute<"/api/openapi">
|
||||||
| StaticRoute<"/api/sentry-example-api">
|
| StaticRoute<"/api/sentry-example-api">
|
||||||
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
||||||
| 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">;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -36,8 +36,6 @@ let config = {
|
|||||||
});
|
});
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
|
||||||
transpilePackages: ["openpipe"],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = nextRoutes()(config);
|
config = nextRoutes()(config);
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -100,8 +100,7 @@
|
|||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vite-tsconfig-paths": "^4.2.0",
|
"vite-tsconfig-paths": "^4.2.0",
|
||||||
"zod": "^3.21.4",
|
"zod": "^3.21.4",
|
||||||
"zustand": "^4.3.9",
|
"zustand": "^4.3.9"
|
||||||
"openpipe": "workspace:*"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@openapi-contrib/openapi-schema-to-json-schema": "^4.0.5",
|
"@openapi-contrib/openapi-schema-to-json-schema": "^4.0.5",
|
||||||
|
|||||||
1565
pnpm-lock.yaml → app/pnpm-lock.yaml
generated
1565
pnpm-lock.yaml → app/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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";
|
||||||
@@ -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");
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "LoggedCall" ADD COLUMN "model" TEXT;
|
|
||||||
@@ -112,10 +112,10 @@ 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?
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = ({
|
||||||
|
|||||||
@@ -107,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 (
|
||||||
@@ -160,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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const ScenarioRow = (props: {
|
|||||||
variants: PromptVariant[];
|
variants: PromptVariant[];
|
||||||
canHide: boolean;
|
canHide: boolean;
|
||||||
rowStart: number;
|
rowStart: number;
|
||||||
isLast: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
@@ -22,12 +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}
|
borderLeftWidth={1}
|
||||||
{...borders}
|
{...borders}
|
||||||
rowStart={props.rowStart}
|
rowStart={props.rowStart}
|
||||||
colStart={1}
|
colStart={1}
|
||||||
borderBottomLeftRadius={props.isLast ? 8 : 0}
|
|
||||||
>
|
>
|
||||||
<ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} />
|
<ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
@@ -37,10 +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}
|
||||||
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} />
|
||||||
|
|||||||
@@ -48,20 +48,7 @@ export const ScenariosHeader = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack w="100%" pb={cellPadding.y} pt={0} align="center" spacing={0}>
|
||||||
w="100%"
|
|
||||||
py={cellPadding.y}
|
|
||||||
px={cellPadding.x}
|
|
||||||
align="center"
|
|
||||||
spacing={0}
|
|
||||||
borderTopRightRadius={8}
|
|
||||||
borderTopLeftRadius={8}
|
|
||||||
bgColor="white"
|
|
||||||
borderWidth={1}
|
|
||||||
borderBottomWidth={0}
|
|
||||||
borderColor="gray.300"
|
|
||||||
mt={8}
|
|
||||||
>
|
|
||||||
<Text fontSize={16} fontWeight="bold">
|
<Text fontSize={16} fontWeight="bold">
|
||||||
Scenarios ({scenarios.data?.count})
|
Scenarios ({scenarios.data?.count})
|
||||||
</Text>
|
</Text>
|
||||||
@@ -70,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)}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -99,7 +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}
|
||||||
isLast={i === visibleScenariosCount - 1}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<GridItem
|
<GridItem
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
201
app/src/components/dashboard/LoggedCallTable.tsx
Normal file
201
app/src/components/dashboard/LoggedCallTable.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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%" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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;
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -8,6 +8,8 @@ import { type CompletionResponse } from "../types";
|
|||||||
import { isArray, isString, omit } from "lodash-es";
|
import { isArray, isString, omit } from "lodash-es";
|
||||||
import { openai } from "~/server/utils/openai";
|
import { openai } from "~/server/utils/openai";
|
||||||
import { APIError } from "openai";
|
import { APIError } from "openai";
|
||||||
|
import frontendModelProvider from "./frontend";
|
||||||
|
import modelProvider, { type SupportedModel } from ".";
|
||||||
|
|
||||||
const mergeStreamedChunks = (
|
const mergeStreamedChunks = (
|
||||||
base: ChatCompletion | null,
|
base: ChatCompletion | null,
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { datasetEntries } from "./routers/datasetEntries.router";
|
|||||||
import { externalApiRouter } from "./routers/externalApi.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.
|
||||||
@@ -30,7 +29,6 @@ export const appRouter = createTRPCRouter({
|
|||||||
datasetEntries: datasetEntries,
|
datasetEntries: datasetEntries,
|
||||||
projects: projectsRouter,
|
projects: projectsRouter,
|
||||||
dashboard: dashboardRouter,
|
dashboard: dashboardRouter,
|
||||||
loggedCalls: loggedCallsRouter,
|
|
||||||
externalApi: externalApiRouter,
|
externalApi: externalApiRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import { hashRequest } from "~/server/utils/hashObject";
|
import { hashRequest } from "~/server/utils/hashObject";
|
||||||
import modelProvider from "~/modelProviders/openai-ChatCompletion";
|
|
||||||
import {
|
|
||||||
type ChatCompletion,
|
|
||||||
type CompletionCreateParams,
|
|
||||||
} from "openai/resources/chat/completions";
|
|
||||||
|
|
||||||
const reqValidator = z.object({
|
const reqValidator = z.object({
|
||||||
model: z.string(),
|
model: z.string(),
|
||||||
@@ -21,6 +16,11 @@ const reqValidator = z.object({
|
|||||||
const respValidator = z.object({
|
const respValidator = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
model: z.string(),
|
model: z.string(),
|
||||||
|
usage: z.object({
|
||||||
|
total_tokens: z.number(),
|
||||||
|
prompt_tokens: z.number(),
|
||||||
|
completion_tokens: z.number(),
|
||||||
|
}),
|
||||||
choices: z.array(
|
choices: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
finish_reason: z.string(),
|
finish_reason: z.string(),
|
||||||
@@ -35,12 +35,11 @@ export const externalApiRouter = createTRPCRouter({
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/check-cache",
|
path: "/v1/check-cache",
|
||||||
description: "Check if a prompt is cached",
|
description: "Check if a prompt is cached",
|
||||||
protect: true,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
requestedAt: z.number().describe("Unix timestamp in milliseconds"),
|
startTime: z.number().describe("Unix timestamp in milliseconds"),
|
||||||
reqPayload: z.unknown().describe("JSON-encoded request payload"),
|
reqPayload: z.unknown().describe("JSON-encoded request payload"),
|
||||||
tags: z
|
tags: z
|
||||||
.record(z.string())
|
.record(z.string())
|
||||||
@@ -70,9 +69,15 @@ export const externalApiRouter = createTRPCRouter({
|
|||||||
const cacheKey = hashRequest(key.projectId, reqPayload as JsonValue);
|
const cacheKey = hashRequest(key.projectId, reqPayload as JsonValue);
|
||||||
|
|
||||||
const existingResponse = await prisma.loggedCallModelResponse.findFirst({
|
const existingResponse = await prisma.loggedCallModelResponse.findFirst({
|
||||||
where: { cacheKey },
|
where: {
|
||||||
include: { originalLoggedCall: true },
|
cacheKey,
|
||||||
orderBy: { requestedAt: "desc" },
|
},
|
||||||
|
include: {
|
||||||
|
originalLoggedCall: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
startTime: "desc",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingResponse) return { respPayload: null };
|
if (!existingResponse) return { respPayload: null };
|
||||||
@@ -80,7 +85,7 @@ export const externalApiRouter = createTRPCRouter({
|
|||||||
await prisma.loggedCall.create({
|
await prisma.loggedCall.create({
|
||||||
data: {
|
data: {
|
||||||
projectId: key.projectId,
|
projectId: key.projectId,
|
||||||
requestedAt: new Date(input.requestedAt),
|
startTime: new Date(input.startTime),
|
||||||
cacheHit: true,
|
cacheHit: true,
|
||||||
modelResponseId: existingResponse.id,
|
modelResponseId: existingResponse.id,
|
||||||
},
|
},
|
||||||
@@ -97,17 +102,16 @@ export const externalApiRouter = createTRPCRouter({
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/report",
|
path: "/v1/report",
|
||||||
description: "Report an API call",
|
description: "Report an API call",
|
||||||
protect: true,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
requestedAt: z.number().describe("Unix timestamp in milliseconds"),
|
startTime: z.number().describe("Unix timestamp in milliseconds"),
|
||||||
receivedAt: z.number().describe("Unix timestamp in milliseconds"),
|
endTime: z.number().describe("Unix timestamp in milliseconds"),
|
||||||
reqPayload: z.unknown().describe("JSON-encoded request payload"),
|
reqPayload: z.unknown().describe("JSON-encoded request payload"),
|
||||||
respPayload: z.unknown().optional().describe("JSON-encoded response payload"),
|
respPayload: z.unknown().optional().describe("JSON-encoded response payload"),
|
||||||
statusCode: z.number().optional().describe("HTTP status code of response"),
|
respStatus: z.number().optional().describe("HTTP status code of response"),
|
||||||
errorMessage: z.string().optional().describe("User-friendly error message"),
|
error: z.string().optional().describe("User-friendly error message"),
|
||||||
tags: z
|
tags: z
|
||||||
.record(z.string())
|
.record(z.string())
|
||||||
.optional()
|
.optional()
|
||||||
@@ -118,7 +122,6 @@ export const externalApiRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
console.log("GOT TAGS", input.tags);
|
|
||||||
const apiKey = ctx.apiKey;
|
const apiKey = ctx.apiKey;
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
@@ -137,41 +140,35 @@ export const externalApiRouter = createTRPCRouter({
|
|||||||
const newLoggedCallId = uuidv4();
|
const newLoggedCallId = uuidv4();
|
||||||
const newModelResponseId = uuidv4();
|
const newModelResponseId = uuidv4();
|
||||||
|
|
||||||
let usage;
|
const usage = respPayload.success ? respPayload.data.usage : undefined;
|
||||||
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([
|
await prisma.$transaction([
|
||||||
prisma.loggedCall.create({
|
prisma.loggedCall.create({
|
||||||
data: {
|
data: {
|
||||||
id: newLoggedCallId,
|
id: newLoggedCallId,
|
||||||
projectId: key.projectId,
|
projectId: key.projectId,
|
||||||
requestedAt: new Date(input.requestedAt),
|
startTime: new Date(input.startTime),
|
||||||
cacheHit: false,
|
cacheHit: false,
|
||||||
model,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.loggedCallModelResponse.create({
|
prisma.loggedCallModelResponse.create({
|
||||||
data: {
|
data: {
|
||||||
id: newModelResponseId,
|
id: newModelResponseId,
|
||||||
originalLoggedCallId: newLoggedCallId,
|
originalLoggedCallId: newLoggedCallId,
|
||||||
requestedAt: new Date(input.requestedAt),
|
startTime: new Date(input.startTime),
|
||||||
receivedAt: new Date(input.receivedAt),
|
endTime: new Date(input.endTime),
|
||||||
reqPayload: input.reqPayload as Prisma.InputJsonValue,
|
reqPayload: input.reqPayload as Prisma.InputJsonValue,
|
||||||
respPayload: input.respPayload as Prisma.InputJsonValue,
|
respPayload: input.respPayload as Prisma.InputJsonValue,
|
||||||
statusCode: input.statusCode,
|
respStatus: input.respStatus,
|
||||||
errorMessage: input.errorMessage,
|
error: input.error,
|
||||||
durationMs: input.receivedAt - input.requestedAt,
|
durationMs: input.endTime - input.startTime,
|
||||||
cacheKey: respPayload.success ? requestHash : null,
|
...(respPayload.success
|
||||||
inputTokens: usage?.inputTokens,
|
? {
|
||||||
outputTokens: usage?.outputTokens,
|
cacheKey: requestHash,
|
||||||
cost: usage?.cost,
|
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
|
// Avoid foreign key constraint error by updating the logged call after the model response is created
|
||||||
@@ -185,14 +182,24 @@ export const externalApiRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const tagsToCreate = Object.entries(input.tags ?? {}).map(([name, value]) => ({
|
if (input.tags) {
|
||||||
loggedCallId: newLoggedCallId,
|
const tagsToCreate = Object.entries(input.tags).map(([name, value]) => ({
|
||||||
// sanitize tags
|
loggedCallId: newLoggedCallId,
|
||||||
name: name.replaceAll(/[^a-zA-Z0-9_]/g, "_"),
|
// sanitize tags
|
||||||
value,
|
name: name.replaceAll(/[^a-zA-Z0-9_]/g, "_"),
|
||||||
}));
|
value,
|
||||||
await prisma.loggedCallTag.createMany({
|
}));
|
||||||
data: tagsToCreate,
|
|
||||||
});
|
if (reqPayload.success) {
|
||||||
|
tagsToCreate.push({
|
||||||
|
loggedCallId: newLoggedCallId,
|
||||||
|
name: "$model",
|
||||||
|
value: reqPayload.data.model,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await prisma.loggedCallTag.createMany({
|
||||||
|
data: tagsToCreate,
|
||||||
|
});
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) };
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
|||||||
// Get the session from the server using the getServerSession wrapper function
|
// Get the session from the server using the getServerSession wrapper function
|
||||||
const session = await getServerAuthSession({ req, res });
|
const session = await getServerAuthSession({ req, res });
|
||||||
|
|
||||||
const apiKey = req.headers.authorization?.split(" ")[1] as string | null;
|
const apiKey = req.headers["x-openpipe-api-key"] as string | null;
|
||||||
|
|
||||||
return createInnerTRPCContext({
|
return createInnerTRPCContext({
|
||||||
session,
|
session,
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
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 });
|
||||||
|
|
||||||
@@ -26,8 +27,6 @@ execSync(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("Generating Python client");
|
|
||||||
|
|
||||||
execSync(path.join(clientLibsPath, "python/codegen.sh"));
|
|
||||||
|
|
||||||
console.log("Done!");
|
console.log("Done!");
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
import { type ClientOptions } from "openai";
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import OpenAI 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" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
31
app/src/utils/analytics/clientAnalytics.ts
Normal file
31
app/src/utils/analytics/clientAnalytics.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -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>;
|
|
||||||
};
|
|
||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
@@ -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 },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#! /bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
source app/.env
|
|
||||||
|
|
||||||
docker build . --file app/Dockerfile
|
|
||||||
@@ -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.
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
""" A client library for accessing OpenPipe API """
|
|
||||||
from .client import AuthenticatedClient, Client
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
"AuthenticatedClient",
|
|
||||||
"Client",
|
|
||||||
)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
""" Contains methods for accessing the API """
|
|
||||||
@@ -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.external_api_check_cache_json_body import ExternalApiCheckCacheJsonBody
|
|
||||||
from ...models.external_api_check_cache_response_200 import ExternalApiCheckCacheResponse200
|
|
||||||
from ...types import Response
|
|
||||||
|
|
||||||
|
|
||||||
def _get_kwargs(
|
|
||||||
*,
|
|
||||||
json_body: ExternalApiCheckCacheJsonBody,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
json_json_body = json_body.to_dict()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"method": "post",
|
|
||||||
"url": "/v1/check-cache",
|
|
||||||
"json": json_json_body,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_response(
|
|
||||||
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
|
|
||||||
) -> Optional[ExternalApiCheckCacheResponse200]:
|
|
||||||
if response.status_code == HTTPStatus.OK:
|
|
||||||
response_200 = ExternalApiCheckCacheResponse200.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[ExternalApiCheckCacheResponse200]:
|
|
||||||
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: ExternalApiCheckCacheJsonBody,
|
|
||||||
) -> Response[ExternalApiCheckCacheResponse200]:
|
|
||||||
"""Check if a prompt is cached
|
|
||||||
|
|
||||||
Args:
|
|
||||||
json_body (ExternalApiCheckCacheJsonBody):
|
|
||||||
|
|
||||||
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[ExternalApiCheckCacheResponse200]
|
|
||||||
"""
|
|
||||||
|
|
||||||
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: ExternalApiCheckCacheJsonBody,
|
|
||||||
) -> Optional[ExternalApiCheckCacheResponse200]:
|
|
||||||
"""Check if a prompt is cached
|
|
||||||
|
|
||||||
Args:
|
|
||||||
json_body (ExternalApiCheckCacheJsonBody):
|
|
||||||
|
|
||||||
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:
|
|
||||||
ExternalApiCheckCacheResponse200
|
|
||||||
"""
|
|
||||||
|
|
||||||
return sync_detailed(
|
|
||||||
client=client,
|
|
||||||
json_body=json_body,
|
|
||||||
).parsed
|
|
||||||
|
|
||||||
|
|
||||||
async def asyncio_detailed(
|
|
||||||
*,
|
|
||||||
client: AuthenticatedClient,
|
|
||||||
json_body: ExternalApiCheckCacheJsonBody,
|
|
||||||
) -> Response[ExternalApiCheckCacheResponse200]:
|
|
||||||
"""Check if a prompt is cached
|
|
||||||
|
|
||||||
Args:
|
|
||||||
json_body (ExternalApiCheckCacheJsonBody):
|
|
||||||
|
|
||||||
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[ExternalApiCheckCacheResponse200]
|
|
||||||
"""
|
|
||||||
|
|
||||||
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: ExternalApiCheckCacheJsonBody,
|
|
||||||
) -> Optional[ExternalApiCheckCacheResponse200]:
|
|
||||||
"""Check if a prompt is cached
|
|
||||||
|
|
||||||
Args:
|
|
||||||
json_body (ExternalApiCheckCacheJsonBody):
|
|
||||||
|
|
||||||
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:
|
|
||||||
ExternalApiCheckCacheResponse200
|
|
||||||
"""
|
|
||||||
|
|
||||||
return (
|
|
||||||
await asyncio_detailed(
|
|
||||||
client=client,
|
|
||||||
json_body=json_body,
|
|
||||||
)
|
|
||||||
).parsed
|
|
||||||
@@ -1,98 +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.external_api_report_json_body import ExternalApiReportJsonBody
|
|
||||||
from ...types import Response
|
|
||||||
|
|
||||||
|
|
||||||
def _get_kwargs(
|
|
||||||
*,
|
|
||||||
json_body: ExternalApiReportJsonBody,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
json_json_body = json_body.to_dict()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"method": "post",
|
|
||||||
"url": "/v1/report",
|
|
||||||
"json": json_json_body,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]:
|
|
||||||
if response.status_code == HTTPStatus.OK:
|
|
||||||
return None
|
|
||||||
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[Any]:
|
|
||||||
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: ExternalApiReportJsonBody,
|
|
||||||
) -> Response[Any]:
|
|
||||||
"""Report an API call
|
|
||||||
|
|
||||||
Args:
|
|
||||||
json_body (ExternalApiReportJsonBody):
|
|
||||||
|
|
||||||
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[Any]
|
|
||||||
"""
|
|
||||||
|
|
||||||
kwargs = _get_kwargs(
|
|
||||||
json_body=json_body,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = client.get_httpx_client().request(
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
return _build_response(client=client, response=response)
|
|
||||||
|
|
||||||
|
|
||||||
async def asyncio_detailed(
|
|
||||||
*,
|
|
||||||
client: AuthenticatedClient,
|
|
||||||
json_body: ExternalApiReportJsonBody,
|
|
||||||
) -> Response[Any]:
|
|
||||||
"""Report an API call
|
|
||||||
|
|
||||||
Args:
|
|
||||||
json_body (ExternalApiReportJsonBody):
|
|
||||||
|
|
||||||
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[Any]
|
|
||||||
"""
|
|
||||||
|
|
||||||
kwargs = _get_kwargs(
|
|
||||||
json_body=json_body,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await client.get_async_httpx_client().request(**kwargs)
|
|
||||||
|
|
||||||
return _build_response(client=client, response=response)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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"]
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
""" Contains all the data models used in inputs/outputs """
|
|
||||||
|
|
||||||
from .external_api_check_cache_json_body import ExternalApiCheckCacheJsonBody
|
|
||||||
from .external_api_check_cache_json_body_tags import ExternalApiCheckCacheJsonBodyTags
|
|
||||||
from .external_api_check_cache_response_200 import ExternalApiCheckCacheResponse200
|
|
||||||
from .external_api_report_json_body import ExternalApiReportJsonBody
|
|
||||||
from .external_api_report_json_body_tags import ExternalApiReportJsonBodyTags
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
"ExternalApiCheckCacheJsonBody",
|
|
||||||
"ExternalApiCheckCacheJsonBodyTags",
|
|
||||||
"ExternalApiCheckCacheResponse200",
|
|
||||||
"ExternalApiReportJsonBody",
|
|
||||||
"ExternalApiReportJsonBodyTags",
|
|
||||||
)
|
|
||||||
@@ -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.external_api_check_cache_json_body_tags import ExternalApiCheckCacheJsonBodyTags
|
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T", bound="ExternalApiCheckCacheJsonBody")
|
|
||||||
|
|
||||||
|
|
||||||
@define
|
|
||||||
class ExternalApiCheckCacheJsonBody:
|
|
||||||
"""
|
|
||||||
Attributes:
|
|
||||||
requested_at (float): Unix timestamp in milliseconds
|
|
||||||
req_payload (Union[Unset, Any]): JSON-encoded request payload
|
|
||||||
tags (Union[Unset, ExternalApiCheckCacheJsonBodyTags]): 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, "ExternalApiCheckCacheJsonBodyTags"] = 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.external_api_check_cache_json_body_tags import ExternalApiCheckCacheJsonBodyTags
|
|
||||||
|
|
||||||
d = src_dict.copy()
|
|
||||||
requested_at = d.pop("requestedAt")
|
|
||||||
|
|
||||||
req_payload = d.pop("reqPayload", UNSET)
|
|
||||||
|
|
||||||
_tags = d.pop("tags", UNSET)
|
|
||||||
tags: Union[Unset, ExternalApiCheckCacheJsonBodyTags]
|
|
||||||
if isinstance(_tags, Unset):
|
|
||||||
tags = UNSET
|
|
||||||
else:
|
|
||||||
tags = ExternalApiCheckCacheJsonBodyTags.from_dict(_tags)
|
|
||||||
|
|
||||||
external_api_check_cache_json_body = cls(
|
|
||||||
requested_at=requested_at,
|
|
||||||
req_payload=req_payload,
|
|
||||||
tags=tags,
|
|
||||||
)
|
|
||||||
|
|
||||||
return external_api_check_cache_json_body
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
from typing import Any, Dict, List, Type, TypeVar
|
|
||||||
|
|
||||||
from attrs import define, field
|
|
||||||
|
|
||||||
T = TypeVar("T", bound="ExternalApiCheckCacheJsonBodyTags")
|
|
||||||
|
|
||||||
|
|
||||||
@define
|
|
||||||
class ExternalApiCheckCacheJsonBodyTags:
|
|
||||||
"""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()
|
|
||||||
external_api_check_cache_json_body_tags = cls()
|
|
||||||
|
|
||||||
external_api_check_cache_json_body_tags.additional_properties = d
|
|
||||||
return external_api_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
|
|
||||||
@@ -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="ExternalApiCheckCacheResponse200")
|
|
||||||
|
|
||||||
|
|
||||||
@define
|
|
||||||
class ExternalApiCheckCacheResponse200:
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
|
|
||||||
external_api_check_cache_response_200 = cls(
|
|
||||||
resp_payload=resp_payload,
|
|
||||||
)
|
|
||||||
|
|
||||||
return external_api_check_cache_response_200
|
|
||||||
@@ -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.external_api_report_json_body_tags import ExternalApiReportJsonBodyTags
|
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T", bound="ExternalApiReportJsonBody")
|
|
||||||
|
|
||||||
|
|
||||||
@define
|
|
||||||
class ExternalApiReportJsonBody:
|
|
||||||
"""
|
|
||||||
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, ExternalApiReportJsonBodyTags]): 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, "ExternalApiReportJsonBodyTags"] = 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.external_api_report_json_body_tags import ExternalApiReportJsonBodyTags
|
|
||||||
|
|
||||||
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, ExternalApiReportJsonBodyTags]
|
|
||||||
if isinstance(_tags, Unset):
|
|
||||||
tags = UNSET
|
|
||||||
else:
|
|
||||||
tags = ExternalApiReportJsonBodyTags.from_dict(_tags)
|
|
||||||
|
|
||||||
external_api_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 external_api_report_json_body
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
from typing import Any, Dict, List, Type, TypeVar
|
|
||||||
|
|
||||||
from attrs import define, field
|
|
||||||
|
|
||||||
T = TypeVar("T", bound="ExternalApiReportJsonBodyTags")
|
|
||||||
|
|
||||||
|
|
||||||
@define
|
|
||||||
class ExternalApiReportJsonBodyTags:
|
|
||||||
"""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()
|
|
||||||
external_api_report_json_body_tags = cls()
|
|
||||||
|
|
||||||
external_api_report_json_body_tags.additional_properties = d
|
|
||||||
return external_api_report_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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# Marker file for PEP 561
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
""" Contains some shared types for properties """
|
|
||||||
from http import HTTPStatus
|
|
||||||
from typing import BinaryIO, Generic, Literal, MutableMapping, Optional, Tuple, TypeVar
|
|
||||||
|
|
||||||
from attrs import define
|
|
||||||
|
|
||||||
|
|
||||||
class Unset:
|
|
||||||
def __bool__(self) -> Literal[False]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
UNSET: Unset = Unset()
|
|
||||||
|
|
||||||
FileJsonType = Tuple[Optional[str], BinaryIO, Optional[str]]
|
|
||||||
|
|
||||||
|
|
||||||
@define
|
|
||||||
class File:
|
|
||||||
"""Contains information for file uploads"""
|
|
||||||
|
|
||||||
payload: BinaryIO
|
|
||||||
file_name: Optional[str] = None
|
|
||||||
mime_type: Optional[str] = None
|
|
||||||
|
|
||||||
def to_tuple(self) -> FileJsonType:
|
|
||||||
"""Return a tuple representation that httpx will accept for multipart/form-data"""
|
|
||||||
return self.file_name, self.payload, self.mime_type
|
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
|
||||||
|
|
||||||
|
|
||||||
@define
|
|
||||||
class Response(Generic[T]):
|
|
||||||
"""A response from an endpoint"""
|
|
||||||
|
|
||||||
status_code: HTTPStatus
|
|
||||||
content: bytes
|
|
||||||
headers: MutableMapping[str, str]
|
|
||||||
parsed: Optional[T]
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["File", "Response", "FileJsonType"]
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
|
|
||||||
def merge_streamed_chunks(base: Optional[Any], chunk: Any) -> Any:
|
|
||||||
if base is None:
|
|
||||||
return merge_streamed_chunks({**chunk, "choices": []}, chunk)
|
|
||||||
|
|
||||||
choices = base["choices"].copy()
|
|
||||||
for choice in chunk["choices"]:
|
|
||||||
base_choice = next((c for c in choices if c["index"] == choice["index"]), None)
|
|
||||||
|
|
||||||
if base_choice:
|
|
||||||
base_choice["finish_reason"] = (
|
|
||||||
choice.get("finish_reason") or base_choice["finish_reason"]
|
|
||||||
)
|
|
||||||
base_choice["message"] = base_choice.get("message") or {"role": "assistant"}
|
|
||||||
|
|
||||||
if choice.get("delta") and choice["delta"].get("content"):
|
|
||||||
base_choice["message"]["content"] = (
|
|
||||||
base_choice["message"].get("content") or ""
|
|
||||||
) + (choice["delta"].get("content") or "")
|
|
||||||
if choice.get("delta") and choice["delta"].get("function_call"):
|
|
||||||
fn_call = base_choice["message"].get("function_call") or {}
|
|
||||||
fn_call["name"] = (fn_call.get("name") or "") + (
|
|
||||||
choice["delta"]["function_call"].get("name") or ""
|
|
||||||
)
|
|
||||||
fn_call["arguments"] = (fn_call.get("arguments") or "") + (
|
|
||||||
choice["delta"]["function_call"].get("arguments") or ""
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Here, we'll have to handle the omitted property "delta" manually
|
|
||||||
new_choice = {k: v for k, v in choice.items() if k != "delta"}
|
|
||||||
choices.append(
|
|
||||||
{**new_choice, "message": {"role": "assistant", **choice["delta"]}}
|
|
||||||
)
|
|
||||||
|
|
||||||
merged = {
|
|
||||||
**base,
|
|
||||||
"choices": choices,
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import openai as original_openai
|
|
||||||
from openai.openai_object import OpenAIObject
|
|
||||||
import time
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
from openpipe.merge_openai_chunks import merge_streamed_chunks
|
|
||||||
|
|
||||||
from .shared import maybe_check_cache, maybe_check_cache_async, report_async, report
|
|
||||||
|
|
||||||
|
|
||||||
class WrappedChatCompletion(original_openai.ChatCompletion):
|
|
||||||
@classmethod
|
|
||||||
def create(cls, *args, **kwargs):
|
|
||||||
openpipe_options = kwargs.pop("openpipe", {})
|
|
||||||
|
|
||||||
cached_response = maybe_check_cache(
|
|
||||||
openpipe_options=openpipe_options, req_payload=kwargs
|
|
||||||
)
|
|
||||||
if cached_response:
|
|
||||||
return OpenAIObject.construct_from(cached_response, api_key=None)
|
|
||||||
|
|
||||||
requested_at = int(time.time() * 1000)
|
|
||||||
|
|
||||||
try:
|
|
||||||
chat_completion = original_openai.ChatCompletion.create(*args, **kwargs)
|
|
||||||
|
|
||||||
if inspect.isgenerator(chat_completion):
|
|
||||||
|
|
||||||
def _gen():
|
|
||||||
assembled_completion = None
|
|
||||||
for chunk in chat_completion:
|
|
||||||
assembled_completion = merge_streamed_chunks(
|
|
||||||
assembled_completion, chunk
|
|
||||||
)
|
|
||||||
yield chunk
|
|
||||||
|
|
||||||
received_at = int(time.time() * 1000)
|
|
||||||
|
|
||||||
report(
|
|
||||||
openpipe_options=openpipe_options,
|
|
||||||
requested_at=requested_at,
|
|
||||||
received_at=received_at,
|
|
||||||
req_payload=kwargs,
|
|
||||||
resp_payload=assembled_completion,
|
|
||||||
status_code=200,
|
|
||||||
)
|
|
||||||
|
|
||||||
return _gen()
|
|
||||||
else:
|
|
||||||
received_at = int(time.time() * 1000)
|
|
||||||
|
|
||||||
report(
|
|
||||||
openpipe_options=openpipe_options,
|
|
||||||
requested_at=requested_at,
|
|
||||||
received_at=received_at,
|
|
||||||
req_payload=kwargs,
|
|
||||||
resp_payload=chat_completion,
|
|
||||||
status_code=200,
|
|
||||||
)
|
|
||||||
|
|
||||||
return chat_completion
|
|
||||||
except Exception as e:
|
|
||||||
received_at = int(time.time() * 1000)
|
|
||||||
|
|
||||||
if isinstance(e, original_openai.OpenAIError):
|
|
||||||
report(
|
|
||||||
openpipe_options=openpipe_options,
|
|
||||||
requested_at=requested_at,
|
|
||||||
received_at=received_at,
|
|
||||||
req_payload=kwargs,
|
|
||||||
resp_payload=e.json_body,
|
|
||||||
error_message=str(e),
|
|
||||||
status_code=e.http_status,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
report(
|
|
||||||
openpipe_options=openpipe_options,
|
|
||||||
requested_at=requested_at,
|
|
||||||
received_at=received_at,
|
|
||||||
req_payload=kwargs,
|
|
||||||
error_message=str(e),
|
|
||||||
)
|
|
||||||
|
|
||||||
raise e
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def acreate(cls, *args, **kwargs):
|
|
||||||
openpipe_options = kwargs.pop("openpipe", {})
|
|
||||||
|
|
||||||
cached_response = await maybe_check_cache_async(
|
|
||||||
openpipe_options=openpipe_options, req_payload=kwargs
|
|
||||||
)
|
|
||||||
if cached_response:
|
|
||||||
return OpenAIObject.construct_from(cached_response, api_key=None)
|
|
||||||
|
|
||||||
requested_at = int(time.time() * 1000)
|
|
||||||
|
|
||||||
try:
|
|
||||||
chat_completion = original_openai.ChatCompletion.acreate(*args, **kwargs)
|
|
||||||
|
|
||||||
if inspect.isgenerator(chat_completion):
|
|
||||||
|
|
||||||
def _gen():
|
|
||||||
assembled_completion = None
|
|
||||||
for chunk in chat_completion:
|
|
||||||
assembled_completion = merge_streamed_chunks(
|
|
||||||
assembled_completion, chunk
|
|
||||||
)
|
|
||||||
yield chunk
|
|
||||||
|
|
||||||
received_at = int(time.time() * 1000)
|
|
||||||
|
|
||||||
report_async(
|
|
||||||
openpipe_options=openpipe_options,
|
|
||||||
requested_at=requested_at,
|
|
||||||
received_at=received_at,
|
|
||||||
req_payload=kwargs,
|
|
||||||
resp_payload=assembled_completion,
|
|
||||||
status_code=200,
|
|
||||||
)
|
|
||||||
|
|
||||||
return _gen()
|
|
||||||
else:
|
|
||||||
received_at = int(time.time() * 1000)
|
|
||||||
|
|
||||||
report_async(
|
|
||||||
openpipe_options=openpipe_options,
|
|
||||||
requested_at=requested_at,
|
|
||||||
received_at=received_at,
|
|
||||||
req_payload=kwargs,
|
|
||||||
resp_payload=chat_completion,
|
|
||||||
status_code=200,
|
|
||||||
)
|
|
||||||
|
|
||||||
return chat_completion
|
|
||||||
except Exception as e:
|
|
||||||
received_at = int(time.time() * 1000)
|
|
||||||
|
|
||||||
if isinstance(e, original_openai.OpenAIError):
|
|
||||||
report_async(
|
|
||||||
openpipe_options=openpipe_options,
|
|
||||||
requested_at=requested_at,
|
|
||||||
received_at=received_at,
|
|
||||||
req_payload=kwargs,
|
|
||||||
resp_payload=e.json_body,
|
|
||||||
error_message=str(e),
|
|
||||||
status_code=e.http_status,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
report_async(
|
|
||||||
openpipe_options=openpipe_options,
|
|
||||||
requested_at=requested_at,
|
|
||||||
received_at=received_at,
|
|
||||||
req_payload=kwargs,
|
|
||||||
error_message=str(e),
|
|
||||||
)
|
|
||||||
|
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
class OpenAIWrapper:
|
|
||||||
ChatCompletion = WrappedChatCompletion()
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
return getattr(original_openai, name)
|
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
|
||||||
return setattr(original_openai, name, value)
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
from openpipe.api_client.api.default import (
|
|
||||||
external_api_report,
|
|
||||||
external_api_check_cache,
|
|
||||||
)
|
|
||||||
from openpipe.api_client.client import AuthenticatedClient
|
|
||||||
from openpipe.api_client.models.external_api_report_json_body_tags import (
|
|
||||||
ExternalApiReportJsonBodyTags,
|
|
||||||
)
|
|
||||||
import toml
|
|
||||||
import time
|
|
||||||
|
|
||||||
version = toml.load("pyproject.toml")["tool"]["poetry"]["version"]
|
|
||||||
|
|
||||||
configured_client = AuthenticatedClient(
|
|
||||||
base_url="https://app.openpipe.ai/api/v1", token=""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_tags(openpipe_options):
|
|
||||||
tags = openpipe_options.get("tags") or {}
|
|
||||||
tags["$sdk"] = "python"
|
|
||||||
tags["$sdk_version"] = version
|
|
||||||
|
|
||||||
return ExternalApiReportJsonBodyTags.from_dict(tags)
|
|
||||||
|
|
||||||
|
|
||||||
def _should_check_cache(openpipe_options):
|
|
||||||
if configured_client.token == "":
|
|
||||||
return False
|
|
||||||
return openpipe_options.get("cache", False)
|
|
||||||
|
|
||||||
|
|
||||||
def _process_cache_payload(
|
|
||||||
payload: external_api_check_cache.ExternalApiCheckCacheResponse200,
|
|
||||||
):
|
|
||||||
if not payload or not payload.resp_payload:
|
|
||||||
return None
|
|
||||||
payload.resp_payload["openpipe"] = {"cache_status": "HIT"}
|
|
||||||
|
|
||||||
return payload.resp_payload
|
|
||||||
|
|
||||||
|
|
||||||
def maybe_check_cache(
|
|
||||||
openpipe_options={},
|
|
||||||
req_payload={},
|
|
||||||
):
|
|
||||||
if not _should_check_cache(openpipe_options):
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
payload = external_api_check_cache.sync(
|
|
||||||
client=configured_client,
|
|
||||||
json_body=external_api_check_cache.ExternalApiCheckCacheJsonBody(
|
|
||||||
req_payload=req_payload,
|
|
||||||
requested_at=int(time.time() * 1000),
|
|
||||||
tags=_get_tags(openpipe_options),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return _process_cache_payload(payload)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# We don't want to break client apps if our API is down for some reason
|
|
||||||
print(f"Error reporting to OpenPipe: {e}")
|
|
||||||
print(e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def maybe_check_cache_async(
|
|
||||||
openpipe_options={},
|
|
||||||
req_payload={},
|
|
||||||
):
|
|
||||||
if not _should_check_cache(openpipe_options):
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
payload = await external_api_check_cache.asyncio(
|
|
||||||
client=configured_client,
|
|
||||||
json_body=external_api_check_cache.ExternalApiCheckCacheJsonBody(
|
|
||||||
req_payload=req_payload,
|
|
||||||
requested_at=int(time.time() * 1000),
|
|
||||||
tags=_get_tags(openpipe_options),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return _process_cache_payload(payload)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# We don't want to break client apps if our API is down for some reason
|
|
||||||
print(f"Error reporting to OpenPipe: {e}")
|
|
||||||
print(e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def report(
|
|
||||||
openpipe_options={},
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
external_api_report.sync_detailed(
|
|
||||||
client=configured_client,
|
|
||||||
json_body=external_api_report.ExternalApiReportJsonBody(
|
|
||||||
**kwargs,
|
|
||||||
tags=_get_tags(openpipe_options),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
# We don't want to break client apps if our API is down for some reason
|
|
||||||
print(f"Error reporting to OpenPipe: {e}")
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
|
|
||||||
async def report_async(
|
|
||||||
openpipe_options={},
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
await external_api_report.asyncio_detailed(
|
|
||||||
client=configured_client,
|
|
||||||
json_body=external_api_report.ExternalApiReportJsonBody(
|
|
||||||
**kwargs,
|
|
||||||
tags=_get_tags(openpipe_options),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
# We don't want to break client apps if our API is down for some reason
|
|
||||||
print(f"Error reporting to OpenPipe: {e}")
|
|
||||||
print(e)
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
from dotenv import load_dotenv
|
|
||||||
from . import openai, configure_openpipe
|
|
||||||
import os
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
openai.api_key = os.getenv("OPENAI_API_KEY")
|
|
||||||
|
|
||||||
configure_openpipe(
|
|
||||||
base_url="http://localhost:3000/api", api_key=os.getenv("OPENPIPE_API_KEY")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_sync():
|
|
||||||
completion = openai.ChatCompletion.create(
|
|
||||||
model="gpt-3.5-turbo",
|
|
||||||
messages=[{"role": "system", "content": "count to 10"}],
|
|
||||||
)
|
|
||||||
|
|
||||||
print(completion.choices[0].message.content)
|
|
||||||
|
|
||||||
|
|
||||||
def test_streaming():
|
|
||||||
completion = openai.ChatCompletion.create(
|
|
||||||
model="gpt-3.5-turbo",
|
|
||||||
messages=[{"role": "system", "content": "count to 10"}],
|
|
||||||
stream=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
for chunk in completion:
|
|
||||||
print(chunk)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_async():
|
|
||||||
acompletion = await openai.ChatCompletion.acreate(
|
|
||||||
model="gpt-3.5-turbo",
|
|
||||||
messages=[{"role": "user", "content": "count down from 5"}],
|
|
||||||
)
|
|
||||||
|
|
||||||
print(acompletion.choices[0].message.content)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_async_streaming():
|
|
||||||
acompletion = await openai.ChatCompletion.acreate(
|
|
||||||
model="gpt-3.5-turbo",
|
|
||||||
messages=[{"role": "user", "content": "count down from 5"}],
|
|
||||||
stream=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
async for chunk in acompletion:
|
|
||||||
print(chunk)
|
|
||||||
|
|
||||||
|
|
||||||
def test_sync_with_tags():
|
|
||||||
completion = openai.ChatCompletion.create(
|
|
||||||
model="gpt-3.5-turbo",
|
|
||||||
messages=[{"role": "system", "content": "count to 10"}],
|
|
||||||
openpipe={"tags": {"promptId": "testprompt"}},
|
|
||||||
)
|
|
||||||
print("finished")
|
|
||||||
|
|
||||||
print(completion.choices[0].message.content)
|
|
||||||
|
|
||||||
|
|
||||||
def test_bad_call():
|
|
||||||
completion = openai.ChatCompletion.create(
|
|
||||||
model="gpt-3.5-turbo-blaster",
|
|
||||||
messages=[{"role": "system", "content": "count to 10"}],
|
|
||||||
stream=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.focus
|
|
||||||
async def test_caching():
|
|
||||||
completion = openai.ChatCompletion.create(
|
|
||||||
model="gpt-3.5-turbo",
|
|
||||||
messages=[{"role": "system", "content": "count to 10"}],
|
|
||||||
openpipe={"cache": True},
|
|
||||||
)
|
|
||||||
|
|
||||||
completion2 = await openai.ChatCompletion.acreate(
|
|
||||||
model="gpt-3.5-turbo",
|
|
||||||
messages=[{"role": "system", "content": "count to 10"}],
|
|
||||||
openpipe={"cache": True},
|
|
||||||
)
|
|
||||||
|
|
||||||
print(completion2)
|
|
||||||
1370
client-libs/python/poetry.lock
generated
1370
client-libs/python/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,35 +0,0 @@
|
|||||||
[tool.poetry]
|
|
||||||
name = "openpipe"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = ""
|
|
||||||
authors = ["Kyle Corbitt <kyle@corbt.com>"]
|
|
||||||
license = "Apache-2.0"
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "^3.9"
|
|
||||||
openai = "^0.27.8"
|
|
||||||
httpx = "^0.24.1"
|
|
||||||
attrs = "^23.1.0"
|
|
||||||
python-dateutil = "^2.8.2"
|
|
||||||
toml = "^0.10.2"
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
openapi-python-client = "^0.15.0"
|
|
||||||
black = "^23.7.0"
|
|
||||||
isort = "^5.12.0"
|
|
||||||
autoflake = "^2.2.0"
|
|
||||||
pytest = "^7.4.0"
|
|
||||||
python-dotenv = "^1.0.0"
|
|
||||||
pytest-asyncio = "^0.21.1"
|
|
||||||
pytest-watch = "^4.2.0"
|
|
||||||
pytest-testmon = "^2.0.12"
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
|
||||||
asyncio_mode = "auto"
|
|
||||||
markers = "focus"
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core>=1.0.0"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
@@ -15,11 +15,6 @@
|
|||||||
"post": {
|
"post": {
|
||||||
"operationId": "externalApi-checkCache",
|
"operationId": "externalApi-checkCache",
|
||||||
"description": "Check if a prompt is cached",
|
"description": "Check if a prompt is cached",
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"Authorization": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
"content": {
|
"content": {
|
||||||
@@ -27,7 +22,7 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"requestedAt": {
|
"startTime": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "Unix timestamp in milliseconds"
|
"description": "Unix timestamp in milliseconds"
|
||||||
},
|
},
|
||||||
@@ -43,7 +38,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"requestedAt"
|
"startTime"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@@ -78,11 +73,6 @@
|
|||||||
"post": {
|
"post": {
|
||||||
"operationId": "externalApi-report",
|
"operationId": "externalApi-report",
|
||||||
"description": "Report an API call",
|
"description": "Report an API call",
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"Authorization": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
"content": {
|
"content": {
|
||||||
@@ -90,11 +80,11 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"requestedAt": {
|
"startTime": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "Unix timestamp in milliseconds"
|
"description": "Unix timestamp in milliseconds"
|
||||||
},
|
},
|
||||||
"receivedAt": {
|
"endTime": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "Unix timestamp in milliseconds"
|
"description": "Unix timestamp in milliseconds"
|
||||||
},
|
},
|
||||||
@@ -104,11 +94,11 @@
|
|||||||
"respPayload": {
|
"respPayload": {
|
||||||
"description": "JSON-encoded response payload"
|
"description": "JSON-encoded response payload"
|
||||||
},
|
},
|
||||||
"statusCode": {
|
"respStatus": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "HTTP status code of response"
|
"description": "HTTP status code of response"
|
||||||
},
|
},
|
||||||
"errorMessage": {
|
"error": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "User-friendly error message"
|
"description": "User-friendly error message"
|
||||||
},
|
},
|
||||||
@@ -121,8 +111,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"requestedAt",
|
"startTime",
|
||||||
"receivedAt"
|
"endTime"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
4
client-libs/typescript/.gitignore
vendored
4
client-libs/typescript/.gitignore
vendored
@@ -1,2 +1,2 @@
|
|||||||
node_modules/
|
node_modules
|
||||||
dist/
|
dist
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user