From b1781058a587ea8f74a047308cbe67b6cc5117de Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman Date: Thu, 4 Apr 2024 00:01:40 -0700 Subject: [PATCH 01/32] Pagination bug --- components/project/eval/eval.tsx | 4 ++-- components/project/eval/prompts.tsx | 4 ++-- lib/services/trace_service.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/project/eval/eval.tsx b/components/project/eval/eval.tsx index 8441bfdc..7b139ddd 100644 --- a/components/project/eval/eval.tsx +++ b/components/project/eval/eval.tsx @@ -62,9 +62,9 @@ export default function Eval({ email }: { email: string }) { } if (result) { if (data) { - setData((prevData: any) => [...prevData, ...result.prompts.result]); + setData((prevData: any) => [...prevData, ...result?.prompts?.result || []]); } else { - setData(result.prompts.result); + setData(result?.prompts?.result || []); } } setPage((currentPage) => currentPage + 1); diff --git a/components/project/eval/prompts.tsx b/components/project/eval/prompts.tsx index bb385183..9ea37d51 100644 --- a/components/project/eval/prompts.tsx +++ b/components/project/eval/prompts.tsx @@ -52,9 +52,9 @@ export default function Prompts({ email }: { email: string }) { } if (result) { if (data) { - setData((prevData: any) => [...prevData, ...result.prompts.result]); + setData((prevData: any) => [...prevData, ...result?.prompts?.result || []]); } else { - setData(result.prompts.result); + setData(result?.prompts?.result || []); } } setPage((currentPage) => currentPage + 1); diff --git a/lib/services/trace_service.ts b/lib/services/trace_service.ts index 1761aa06..ad7c35c7 100644 --- a/lib/services/trace_service.ts +++ b/lib/services/trace_service.ts @@ -244,7 +244,7 @@ export class TraceService implements ITraceService { const md = { page, page_size: pageSize, total_pages: totalPages }; if (page! > totalPages) { - throw Error("Page number is greater than total pages"); + page = totalPages; } const query = sql.select( `* FROM ${project_id} WHERE attributes LIKE '%${attribute}%' ORDER BY 'start_time' DESC LIMIT ${pageSize} OFFSET ${ @@ -325,7 +325,7 @@ export class TraceService implements ITraceService { const md = { page, page_size: pageSize, total_pages: totalPages }; if (page! > totalPages) { - throw Error("Page number is greater than total pages"); + page = totalPages; } const query = sql.select( `* FROM ${project_id} ORDER BY 'createdAt' DESC LIMIT ${pageSize} OFFSET ${ From 0e963e226af17ffdba6282496d1979c6e30c20ad Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman Date: Thu, 4 Apr 2024 17:13:49 -0700 Subject: [PATCH 02/32] Bug fix --- app/api/user/route.ts | 1 - components/project/traces.tsx | 2 -- lib/utils.ts | 1 + 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/api/user/route.ts b/app/api/user/route.ts index d7e95f34..a10d34bf 100644 --- a/app/api/user/route.ts +++ b/app/api/user/route.ts @@ -83,7 +83,6 @@ export async function PUT(req: NextRequest) { } if ("status" in data) { - console.log("updating status"); const user = await prisma.user.update({ where: { id, diff --git a/components/project/traces.tsx b/components/project/traces.tsx index 523aa352..1f56c03e 100644 --- a/components/project/traces.tsx +++ b/components/project/traces.tsx @@ -80,8 +80,6 @@ export default function Traces({ email }: { email: string }) { } // Merge the new data with the existing data - console.log("currentData", currentData); - console.log("newData", newData); if (currentData.length > 0) { const updatedData = [...currentData, ...newData]; diff --git a/lib/utils.ts b/lib/utils.ts index 584d22f2..2c6fd519 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -326,6 +326,7 @@ export function calculatePriceFromUsage( output_tokens: number; } ): any { + if (!model) return { total: 0, input: 0, output: 0 }; if (vendor === "openai") { const costTable = OPENAI_PRICING[model.includes("gpt-4") ? "gpt-4" : model]; if (costTable) { From 7eebf62f5a90163cadcb0e6c67ee63c340a518d1 Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman <105607645+karthikscale3@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:19:27 -0700 Subject: [PATCH 03/32] Update README.md (#76) Update screenshot --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b2c6a731..17cfe17a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ Langtrace is an open source observability software which lets you capture, debug and analyze traces and metrics from all your applications that leverages LLM APIs, Vector Databases and LLM based Frameworks. -![image](public/eval.png) +![image](https://github.com/Scale3-Labs/langtrace/assets/105607645/6825158c-39bb-4270-b1f9-446c36c066ee) + ## Open Telemetry Support From b8c18701b65a8578b8fd6c511aec1b0596a14b13 Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman <105607645+karthikscale3@users.noreply.github.com> Date: Mon, 29 Apr 2024 21:21:39 -0700 Subject: [PATCH 04/32] Update README.md (#77) Add reference architecture --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 17cfe17a..125b4b71 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,13 @@ Langtrace automatically captures traces from the following vendors: | ChromaDB | Vector Database | :white_check_mark: | :white_check_mark: | | QDrant | Vector Database | :x: | :white_check_mark: | +--- + +## Langtrace System Architecture + +![image](https://github.com/Scale3-Labs/langtrace/assets/105607645/eae180dd-ebf7-4792-b076-23f75d3734a8) + + --- ## Feature Requests and Issues From c46453c3070e5a4e1541efcbf4d6387e58ae7ef5 Mon Sep 17 00:00:00 2001 From: darshit-s3 <119623510+darshit-s3@users.noreply.github.com> Date: Mon, 6 May 2024 09:36:19 +0530 Subject: [PATCH 05/32] Release (#80) * chore: add docker cmd * Compatibility fixes for SDK version 2.0.0 (#69) * Pagination bug * Bug fix * Fix for schema changes * Render tool calling * Support for Langgraph, Qdrant & Groq (#73) * Pagination bug * Bug fix * Add langgraph support * QDrant support * Add Groq support * update README * update README * feat: optimise docker image for self host setup * adding api access to traces endpoint * clean up * refactor * feat: add clickhouse db create on app start (#79) --------- Co-authored-by: Karthik Kalyanaraman <105607645+karthikscale3@users.noreply.github.com> Co-authored-by: dylan Co-authored-by: dylanzuber-scale3 <116033320+dylanzuber-scale3@users.noreply.github.com> --- .env | 4 +- .github/workflows/build-docker.yaml | 2 + .github/workflows/build-image.yaml | 55 ---- Dockerfile | 31 ++- app/api/data/route.ts | 2 +- app/api/traces/route.ts | 30 +- docker-compose.yaml | 26 +- package-lock.json | 409 +++++++++++++++++++++++++++- package.json | 6 +- scripts/create-clickhouse-db.ts | 31 +++ 10 files changed, 513 insertions(+), 83 deletions(-) delete mode 100644 .github/workflows/build-image.yaml create mode 100644 scripts/create-clickhouse-db.ts diff --git a/.env b/.env index d67882e9..367ef82d 100644 --- a/.env +++ b/.env @@ -21,9 +21,9 @@ NEXTAUTH_URL_INTERNAL="${NEXT_PUBLIC_HOST}" CLICK_HOUSE_HOST="http://langtrace-clickhouse:8123" CLICK_HOUSE_USER="default" CLICK_HOUSE_PASSWORD="" -CLICK_HOUSE_DATABASE_NAME="langtrace_dev" +CLICK_HOUSE_DATABASE_NAME="langtrace_traces" # Admin login ADMIN_EMAIL="admin@langtrace.ai" ADMIN_PASSWORD="langtraceadminpw" -NEXT_PUBLIC_ENABLE_ADMIN_LOGIN="true" \ No newline at end of file +NEXT_PUBLIC_ENABLE_ADMIN_LOGIN="true" diff --git a/.github/workflows/build-docker.yaml b/.github/workflows/build-docker.yaml index 38aeeed7..b1bce0dd 100644 --- a/.github/workflows/build-docker.yaml +++ b/.github/workflows/build-docker.yaml @@ -53,6 +53,7 @@ jobs: context: . file: ./Dockerfile platforms: linux/amd64,linux/arm64 + target: production push: true tags: | ${{ env.DOCKER_REGISTRY }}:${{ inputs.imageTag }} @@ -67,6 +68,7 @@ jobs: context: . file: ./Dockerfile platforms: linux/amd64,linux/arm64 + target: production push: true tags: ${{ env.DOCKER_REGISTRY }}:${{ inputs.imageTag }} labels: ${{ inputs.imageTag }} diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml deleted file mode 100644 index 157e5781..00000000 --- a/.github/workflows/build-image.yaml +++ /dev/null @@ -1,55 +0,0 @@ -name: "Build Docker Image" -run-name: "Version: ${{ inputs.imageTag }} --> Latest: ${{ inputs.isLatest }}" - -on: - workflow_dispatch: - inputs: - imageTag: - description: Release version - required: true - default: example - isLatest: - description: Is this the latest version? - type: boolean - required: true - default: false - -jobs: - docker-build: - runs-on: ubuntu-latest - env: - DOCKER_REGISTRY: scale3labs/langtrace-client - steps: - - name: Github Checkout - # v4.1.1 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - - - name: Log in to Docker Hub - # v3.1.0 - uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push Docker image with latest tag - # v5.3.0 - uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 - with: - context: . - file: ./Dockerfile - push: true - tags: | - ${{ env.DOCKER_REGISTRY }}:${{ inputs.imageTag }} - ${{ env.DOCKER_REGISTRY }}:latest - labels: ${{ inputs.imageTag }} - if: ${{ inputs.isLatest }} - - - name: Build and push Docker image without latest tag - # v5.3.0 - uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 - with: - context: . - file: ./Dockerfile - push: true - tags: ${{ env.DOCKER_REGISTRY }}:${{ inputs.imageTag }} - if: ${{ !inputs.isLatest }} diff --git a/Dockerfile b/Dockerfile index 27888e67..c7cdf011 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Debian based node 21.6 image -FROM node:21.6-bookworm +FROM node:21.6-bookworm as development LABEL maintainer="Langtrace AI " LABEL version="1.0" @@ -16,4 +16,31 @@ RUN npm install EXPOSE 3000 -CMD [ "/bin/sh", "-c", "npm run create-tables && npm run dev" ] +CMD [ "/bin/sh", "-c", "npm run dev" ] + + +# Intermediate image for building the application +FROM development as builder + +WORKDIR /app + +RUN NEXT_PUBLIC_ENABLE_ADMIN_LOGIN=true npm run build + +# Final release image +FROM node:21.6-bookworm as production + +WORKDIR /app + +# Copy only the necessary files +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/package.json . +COPY --from=builder /app/public ./public +COPY --from=builder /app/scripts ./scripts + +# Install only production dependencies +RUN npm install --only=production --omit=dev + +CMD [ "/bin/sh", "-c", "npm start" ] + +EXPOSE 3000 diff --git a/app/api/data/route.ts b/app/api/data/route.ts index 6c4d3db7..89c907cc 100644 --- a/app/api/data/route.ts +++ b/app/api/data/route.ts @@ -106,7 +106,7 @@ export async function DELETE(req: NextRequest) { const data = await req.json(); const { id } = data; - const result = await prisma.data.delete({ + await prisma.data.delete({ where: { id, }, diff --git a/app/api/traces/route.ts b/app/api/traces/route.ts index 0c045993..64034c71 100644 --- a/app/api/traces/route.ts +++ b/app/api/traces/route.ts @@ -1,5 +1,7 @@ import { authOptions } from "@/lib/auth/options"; +import prisma from "@/lib/prisma"; import { TraceService } from "@/lib/services/trace_service"; +import { hashApiKey } from "@/lib/utils"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; import { NextRequest, NextResponse } from "next/server"; @@ -7,12 +9,34 @@ import { NextRequest, NextResponse } from "next/server"; export async function POST(req: NextRequest) { try { const session = await getServerSession(authOptions); - + const apiKey = req.headers.get("x-api-key"); + const { page, pageSize, projectId, filters } = await req.json(); if (!session || !session.user) { - redirect("/login"); + if (apiKey) { + const project = await prisma.project.findFirst({ + where: { + id: projectId, + }, + }); + + if (!project) { + return NextResponse.json( + { error: "No projects found" }, + { status: 404 } + ); + } + + if (apiKey && project.apiKeyHash !== hashApiKey(apiKey)) { + return NextResponse.json( + { error: "Unauthorized. Invalid API key" }, + { status: 401 } + ); + } + } else { + redirect("/login"); + } } - const { page, pageSize, projectId, filters } = await req.json(); const traceService = new TraceService(); const traces = await traceService.GetTracesInProjectPaginated( projectId, diff --git a/docker-compose.yaml b/docker-compose.yaml index 078e6d4e..5e3d6e3d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: "3.8" - services: langtrace-app: container_name: langtrace @@ -7,20 +5,23 @@ services: build: context: . dockerfile: Dockerfile + target: development working_dir: /app env_file: - .env ports: - "3000:3000" + command: npm run dev # Uncmment this for development # volumes: # - .:/app + restart: on-failure:5 depends_on: postgres-db: condition: service_started required: true - clickhouse-init: - condition: service_completed_successfully + clickhouse-db: + condition: service_healthy required: true postgres-db: @@ -48,16 +49,13 @@ services: volumes: - clickhouse-data:/var/lib/clickhouse - clickhouse-init: - container_name: langtrace-clickhouse-init - image: clickhouse/clickhouse-server:23.3.20.27-alpine - env_file: - - .env - depends_on: - clickhouse-db: - condition: service_healthy - required: true - command: clickhouse-client --host langtrace-clickhouse --query 'CREATE DATABASE IF NOT EXISTS $CLICK_HOUSE_DATABASE_NAME;' + langtrace-app-prod: + extends: + service: langtrace-app + build: + target: production + profiles: + - production volumes: postgres-data: diff --git a/package-lock.json b/package-lock.json index c936e48f..92389943 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,6 +82,7 @@ "stream-to-array": "^2.3.0", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", + "tsx": "^4.8.2", "zod": "^3.22.4" }, "devDependencies": { @@ -949,6 +950,351 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -6055,6 +6401,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -6948,10 +7331,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", - "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", - "dev": true, + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz", + "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -11576,7 +11958,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } @@ -12654,6 +13035,24 @@ "resolved": "https://registry.npmjs.org/tsv/-/tsv-0.2.0.tgz", "integrity": "sha512-GG6xbOP85giXXom0dS6z9uyDsxktznjpa1AuDlPrIXDqDnbhjr9Vk6Us8iz6U1nENL4CPS2jZDvIjEdaZsmc4Q==" }, + "node_modules/tsx": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.9.0.tgz", + "integrity": "sha512-UY0UUhDPL6MkqkZU4xTEjEBOLfV+RIt4xeeJ1qwK73xai4/zveG+X6+tieILa7rjtegUW2LE4p7fw7gAoLuytA==", + "dependencies": { + "esbuild": "~0.20.2", + "get-tsconfig": "^4.7.3" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", diff --git a/package.json b/package.json index 11557f41..8b78c400 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,13 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", "build": "prisma generate && next build", "create-tables": "npx prisma db push", "seed-db": "npx prisma db seed", + "create-clickhouse-db": "npx tsx scripts/create-clickhouse-db.ts", + "predev": "npm run create-tables && npm run create-clickhouse-db", + "dev": "next dev", + "prestart": "npm run create-tables && npm run create-clickhouse-db", "start": "next start", "lint": "next lint" }, @@ -85,6 +88,7 @@ "stream-to-array": "^2.3.0", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", + "tsx": "^4.8.2", "zod": "^3.22.4" }, "devDependencies": { diff --git a/scripts/create-clickhouse-db.ts b/scripts/create-clickhouse-db.ts new file mode 100644 index 00000000..e03aa219 --- /dev/null +++ b/scripts/create-clickhouse-db.ts @@ -0,0 +1,31 @@ +import { createClient } from "@clickhouse/client"; + +import { loadEnvConfig } from "@next/env"; +loadEnvConfig(process.cwd()); + +const chClient = createClient({ + database: "default", + host: process.env.CLICK_HOUSE_HOST, + username: process.env.CLICK_HOUSE_USER, + password: process.env.CLICK_HOUSE_PASSWORD, + compression: { + response: true, + }, + clickhouse_settings: { + async_insert: 1, + wait_for_async_insert: 1, + }, +}); + +console.log("Creating Clickhouse DB if not exists..."); +chClient + .query({ + query: `CREATE DATABASE IF NOT EXISTS ${process.env.CLICK_HOUSE_DATABASE_NAME}`, + }) + .then((res) => { + console.log(res.query_id); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); From 8325b6d4323188f1602055b91c15c062841cf8c0 Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman <105607645+karthikscale3@users.noreply.github.com> Date: Tue, 7 May 2024 10:24:20 -0700 Subject: [PATCH 06/32] Release 1.3.0 (#85) * Pagination bug * Bug fix * chore: add docker cmd * Compatibility fixes for SDK version 2.0.0 (#69) * Pagination bug * Bug fix * Fix for schema changes * Render tool calling * Support for Langgraph, Qdrant & Groq (#73) * Pagination bug * Bug fix * Add langgraph support * QDrant support * Add Groq support * update README * update README * feat: optimise docker image for self host setup * adding api access to traces endpoint * clean up * refactor * feat: add clickhouse db create on app start (#79) * docs: add railway deploy, fix sdk badges (#81) * Playground and Prompt Management (#83) * Pagination bug * Bug fix * Playground - basic implementation * Playground - streaming and nonstreaming * Move playground inside project * API key flow * Api key * Playground refactor * Add chat hookup * anthropic streaming support * Bug fixes to openai playground * Anthropic bugfixes * Anthropic bugfix * Cohere first iteration * Cohere role fixes * Cohere api fix * Parallel running * Playground cost calculation non streaming * playground - streaming token calculation * latency and cost * Support for Groq * Add model name * Prompt management views * Remove current promptset flow * Prompt management API hooks * Prompt registry final * Playground bugfixes * Bug fix playground * Rearrange project nav * Fix playground * Fix prompts * Bugfixes * Minor fix * Prompt versioning bugfix * Bugfix * fix: clickhouse table find queries (#82) * Fix to surface multiple LLM requests inside LLM View (#84) * Pagination bug * Bug fix * Fix for surfacing multiple LLM requests in LLMView --------- Co-authored-by: Darshit Suratwala Co-authored-by: darshit-s3 <119623510+darshit-s3@users.noreply.github.com> Co-authored-by: dylan Co-authored-by: dylanzuber-scale3 <116033320+dylanzuber-scale3@users.noreply.github.com> --- README.md | 14 +- .../project/[project_id]/datasets/page.tsx | 4 +- .../promptset/[promptset_id]/page.tsx | 183 -- .../project/[project_id]/playground/page.tsx | 101 + .../[project_id]/prompts/[prompt_id]/page.tsx | 247 ++ .../[project_id]/prompts/page-client.tsx | 36 +- .../project/[project_id]/prompts/page.tsx | 4 +- .../prompts/prompt-management.tsx | 51 +- app/(protected)/projects/page-client.tsx | 10 +- app/(protected)/settings/keys/page-client.tsx | 89 + app/(protected)/settings/keys/page.tsx | 23 + app/api/chat/anthropic/route.ts | 37 + app/api/chat/cohere/route.ts | 40 + app/api/chat/groq/route.ts | 37 + app/api/chat/openai/route.ts | 37 + app/api/prompt/route.ts | 166 +- app/api/promptdata/route.ts | 123 - app/api/promptset/route.ts | 15 +- app/api/span-prompt/route.ts | 66 + components/evaluations/evaluation-row.tsx | 6 +- components/evaluations/evaluation-table.tsx | 2 - components/playground/chat-handlers.ts | 301 ++ components/playground/common.tsx | 169 ++ components/playground/llmchat.tsx | 561 ++++ components/playground/model-dropdown.tsx | 86 + components/playground/settings-sheet.tsx | 2532 +++++++++++++++++ components/project/dataset/create-data.tsx | 128 +- components/project/dataset/create.tsx | 26 +- components/project/dataset/data-set.tsx | 8 +- components/project/dataset/edit-data.tsx | 232 +- components/project/dataset/edit.tsx | 46 +- components/project/dataset/parent.tsx | 39 - components/project/traces/trace-row.tsx | 17 +- components/settings/tabs.tsx | 5 + components/shared/add-api-key.tsx | 174 ++ components/shared/add-to-promptset.tsx | 191 -- components/shared/code-block.tsx | 18 + components/shared/create-prompt-dialog.tsx | 369 +++ components/shared/diff-view.tsx | 49 + components/shared/header.tsx | 18 +- components/shared/llm-picker.tsx | 72 + components/shared/llm-view.tsx | 19 +- components/shared/nav.tsx | 17 +- components/ui/sheet.tsx | 140 + docker-compose.yaml | 4 +- lib/constants.ts | 53 +- lib/services/query_builder_service.ts | 10 +- lib/types/playground_types.ts | 261 ++ lib/utils.ts | 21 + package.json | 12 +- .../migration.sql | 5 + .../migration.sql | 11 + .../migration.sql | 9 + prisma/schema.prisma | 22 +- public/spinner-dark.svg | 26 + public/spinner-light.svg | 26 + 56 files changed, 5864 insertions(+), 1104 deletions(-) delete mode 100644 app/(protected)/project/[project_id]/datasets/promptset/[promptset_id]/page.tsx create mode 100644 app/(protected)/project/[project_id]/playground/page.tsx create mode 100644 app/(protected)/project/[project_id]/prompts/[prompt_id]/page.tsx rename components/project/dataset/prompt-set.tsx => app/(protected)/project/[project_id]/prompts/prompt-management.tsx (63%) create mode 100644 app/(protected)/settings/keys/page-client.tsx create mode 100644 app/(protected)/settings/keys/page.tsx create mode 100644 app/api/chat/anthropic/route.ts create mode 100644 app/api/chat/cohere/route.ts create mode 100644 app/api/chat/groq/route.ts create mode 100644 app/api/chat/openai/route.ts delete mode 100644 app/api/promptdata/route.ts create mode 100644 app/api/span-prompt/route.ts create mode 100644 components/playground/chat-handlers.ts create mode 100644 components/playground/common.tsx create mode 100644 components/playground/llmchat.tsx create mode 100644 components/playground/model-dropdown.tsx create mode 100644 components/playground/settings-sheet.tsx delete mode 100644 components/project/dataset/parent.tsx create mode 100644 components/shared/add-api-key.tsx delete mode 100644 components/shared/add-to-promptset.tsx create mode 100644 components/shared/code-block.tsx create mode 100644 components/shared/create-prompt-dialog.tsx create mode 100644 components/shared/diff-view.tsx create mode 100644 components/shared/llm-picker.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 lib/types/playground_types.ts create mode 100644 prisma/migrations/20240505002132_prompt_management/migration.sql create mode 100644 prisma/migrations/20240505011630_prompt_management_v2/migration.sql create mode 100644 prisma/migrations/20240506153246_prompt_management_mark_live/migration.sql create mode 100644 public/spinner-dark.svg create mode 100644 public/spinner-light.svg diff --git a/README.md b/README.md index 125b4b71..d8f2e5bc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ ## Open Source & Open Telemetry(OTEL) Observability for LLM applications -![Static Badge](https://img.shields.io/badge/License-AGPL--3.0-blue) ![Static Badge](https://img.shields.io/badge/npm_@langtrase/typescript--sdk-1.2.9-green) ![Static Badge](https://img.shields.io/badge/pip_langtrace--python--sdk-1.2.8-green) ![Static Badge](https://img.shields.io/badge/Development_status-Active-green) +![Static Badge](https://img.shields.io/badge/License-AGPL--3.0-blue) +![NPM Version](https://img.shields.io/npm/v/%40langtrase%2Ftypescript-sdk?style=flat&logo=npm&label=%40langtrase%2Ftypescript-sdk&color=green&link=https%3A%2F%2Fgithub.com%2FScale3-Labs%2Flangtrace-typescript-sdk) +![PyPI - Version](https://img.shields.io/pypi/v/langtrace-python-sdk?style=flat&logo=python&label=langtrace-python-sdk&color=green&link=https%3A%2F%2Fgithub.com%2FScale3-Labs%2Flangtrace-python-sdk) +![Static Badge](https://img.shields.io/badge/Development_status-Active-green) +[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/yZGbfC?referralCode=MA2S9H) --- @@ -10,7 +14,6 @@ Langtrace is an open source observability software which lets you capture, debug ![image](https://github.com/Scale3-Labs/langtrace/assets/105607645/6825158c-39bb-4270-b1f9-446c36c066ee) - ## Open Telemetry Support The traces generated by Langtrace adhere to [Open Telemetry Standards(OTEL)](https://opentelemetry.io/docs/concepts/signals/traces/). We are developing [semantic conventions](https://opentelemetry.io/docs/concepts/semantic-conventions/) for the traces generated by this project. You can checkout the current definitions in [this repository](https://github.com/Scale3-Labs/langtrace-trace-attributes/tree/main/schemas). Note: This is an ongoing development and we encourage you to get involved and welcome your feedback. @@ -73,6 +76,9 @@ To run the Langtrace locally, you have to run three services: - Postgres database - Clickhouse database +> [!IMPORTANT] +> Checkout [documentation](https://docs.langtrace.ai/hosting/overview) for various deployment options and configurations. + Requirements: - Docker @@ -94,7 +100,7 @@ The application will be available at `http://localhost:3000`. > if you wish to build the docker image locally and use it, run the docker compose up command with the `--build` flag. > [!TIP] -> to manually pull the docker image from docker hub, run the following command: +> to manually pull the docker image from [docker hub](https://hub.docker.com/r/scale3labs/langtrace-client/tags), run the following command: > > ```bash > docker pull scale3labs/langtrace-client:latest @@ -193,6 +199,7 @@ Either you **update the docker compose version** OR **remove the depends_on prop If clickhouse server is not starting, it is likely that the port 8123 is already in use. You can change the port in the docker-compose file. +
Install the langtrace SDK in your application by following the same instructions under the Langtrace Cloud section above for sending traces to your self hosted setup. --- @@ -228,7 +235,6 @@ Langtrace automatically captures traces from the following vendors: ![image](https://github.com/Scale3-Labs/langtrace/assets/105607645/eae180dd-ebf7-4792-b076-23f75d3734a8) - --- ## Feature Requests and Issues diff --git a/app/(protected)/project/[project_id]/datasets/page.tsx b/app/(protected)/project/[project_id]/datasets/page.tsx index ca6b389b..2cc177a3 100644 --- a/app/(protected)/project/[project_id]/datasets/page.tsx +++ b/app/(protected)/project/[project_id]/datasets/page.tsx @@ -1,4 +1,4 @@ -import Parent from "@/components/project/dataset/parent"; +import DataSet from "@/components/project/dataset/data-set"; import { authOptions } from "@/lib/auth/options"; import { Metadata } from "next"; import { getServerSession } from "next-auth"; @@ -18,7 +18,7 @@ export default async function Page() { return ( <> - + ); } diff --git a/app/(protected)/project/[project_id]/datasets/promptset/[promptset_id]/page.tsx b/app/(protected)/project/[project_id]/datasets/promptset/[promptset_id]/page.tsx deleted file mode 100644 index 5390ee95..00000000 --- a/app/(protected)/project/[project_id]/datasets/promptset/[promptset_id]/page.tsx +++ /dev/null @@ -1,183 +0,0 @@ -"use client"; - -import { CreatePrompt } from "@/components/project/dataset/create-data"; -import { EditPrompt } from "@/components/project/dataset/edit-data"; -import { Spinner } from "@/components/shared/spinner"; -import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; -import { Skeleton } from "@/components/ui/skeleton"; -import { PAGE_SIZE } from "@/lib/constants"; -import { Prompt } from "@prisma/client"; -import { ChevronLeft } from "lucide-react"; -import { useParams } from "next/navigation"; -import { useState } from "react"; -import { useBottomScrollListener } from "react-bottom-scroll-listener"; -import { useQuery } from "react-query"; -import { toast } from "sonner"; - -export default function Promptset() { - const promptset_id = useParams()?.promptset_id as string; - const [page, setPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [showLoader, setShowLoader] = useState(false); - const [currentData, setCurrentData] = useState([]); - - useBottomScrollListener(() => { - if (fetchPromptset.isRefetching) { - return; - } - if (page <= totalPages) { - setShowLoader(true); - fetchPromptset.refetch(); - } - }); - - const fetchPromptset = useQuery({ - queryKey: [promptset_id], - queryFn: async () => { - const response = await fetch( - `/api/promptset?promptset_id=${promptset_id}&page=${page}&pageSize=${PAGE_SIZE}` - ); - if (!response.ok) { - const error = await response.json(); - throw new Error(error?.message || "Failed to fetch promptset"); - } - const result = await response.json(); - return result; - }, - onSuccess: (data) => { - // Get the newly fetched data and metadata - const newData: Prompt[] = data?.promptsets?.Prompt || []; - const metadata = data?.metadata || {}; - - // Update the total pages and current page number - setTotalPages(parseInt(metadata?.total_pages) || 1); - if (parseInt(metadata?.page) <= parseInt(metadata?.total_pages)) { - setPage(parseInt(metadata?.page) + 1); - } - - // Merge the new data with the existing data - if (currentData.length > 0) { - const updatedData = [...currentData, ...newData]; - - // Remove duplicates - const uniqueData = updatedData.filter( - (v: any, i: number, a: any) => - a.findIndex((t: any) => t.id === v.id) === i - ); - - setCurrentData(uniqueData); - } else { - setCurrentData(newData); - } - setShowLoader(false); - }, - onError: (error) => { - setShowLoader(false); - toast.error("Failed to fetch promptset", { - description: error instanceof Error ? error.message : String(error), - }); - }, - }); - - if (fetchPromptset.isLoading || !fetchPromptset.data || !currentData) { - return ; - } else { - return ( -
-
- - -
-
-
-

Created at

-

Value

-

Note

-

-
- {!fetchPromptset.isLoading && currentData.length === 0 && ( -
-

- No prompts found in this promptset -

-
- )} - {!fetchPromptset.isLoading && - currentData.length > 0 && - currentData.map((prompt: any, i: number) => { - return ( -
-
-

{prompt.createdAt}

-

{prompt.value}

-

{prompt.note}

-
- -
-
- -
- ); - })} - {showLoader && ( -
- -
- )} -
-
- ); - } -} - -function PageSkeleton() { - return ( -
-
- - -
-
-
-

Created at

-

Value

-

Note

-

-
- {Array.from({ length: 3 }).map((_, index) => ( - - ))} -
-
- ); -} - -function PromptsetRowSkeleton() { - return ( -
-
-
- -
-
- -
-
- -
-
- -
- ); -} diff --git a/app/(protected)/project/[project_id]/playground/page.tsx b/app/(protected)/project/[project_id]/playground/page.tsx new file mode 100644 index 00000000..56731b7d --- /dev/null +++ b/app/(protected)/project/[project_id]/playground/page.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { AddLLMChat } from "@/components/playground/common"; +import LLMChat from "@/components/playground/llmchat"; +import { + AnthropicModel, + AnthropicSettings, + ChatInterface, + CohereSettings, + GroqSettings, + OpenAIChatInterface, + OpenAIModel, + OpenAISettings, +} from "@/lib/types/playground_types"; +import Link from "next/link"; +import { useState } from "react"; +import { v4 as uuidv4 } from "uuid"; + +export default function Page() { + const [llms, setLLMs] = useState([]); + + const handleRemove = (id: string) => { + setLLMs((currentLLMs) => currentLLMs.filter((llm) => llm.id !== id)); + }; + + const handleAdd = (vendor: string) => { + if (vendor === "openai") { + const settings: OpenAISettings = { + messages: [], + model: "gpt-3.5-turbo" as OpenAIModel, + }; + const openaiChat: OpenAIChatInterface = { + id: uuidv4(), + vendor: "openai", + settings: settings, + }; + setLLMs((currentLLMs) => [...currentLLMs, openaiChat]); + } else if (vendor === "anthropic") { + const settings: AnthropicSettings = { + messages: [], + model: "claude-3-opus-20240229" as AnthropicModel, + maxTokens: 100, + }; + const anthropicChat: ChatInterface = { + id: uuidv4(), + vendor: "anthropic", + settings: settings, + }; + setLLMs((currentLLMs) => [...currentLLMs, anthropicChat]); + } else if (vendor === "cohere") { + const settings: CohereSettings = { + messages: [], + model: "command-r-plus", + }; + const cohereChat: ChatInterface = { + id: uuidv4(), + vendor: "cohere", + settings: settings, + }; + setLLMs((currentLLMs) => [...currentLLMs, cohereChat]); + } else if (vendor === "groq") { + const settings: GroqSettings = { + messages: [], + model: "llama3-8b-8192", + }; + const cohereChat: ChatInterface = { + id: uuidv4(), + vendor: "groq", + settings: settings, + }; + setLLMs((currentLLMs) => [...currentLLMs, cohereChat]); + } + }; + + return ( +
+ + Note: Don't forget to add your LLM provider API keys in the{" "} + + settings page. + + +
+ {llms.map((llm: ChatInterface) => ( + { + const newLLMs = llms.map((l) => + l.id === llm.id ? updatedLLM : l + ); + setLLMs(newLLMs); + }} + onRemove={() => handleRemove(llm.id)} + /> + ))} + handleAdd(vendor)} /> +
+
+ ); +} diff --git a/app/(protected)/project/[project_id]/prompts/[prompt_id]/page.tsx b/app/(protected)/project/[project_id]/prompts/[prompt_id]/page.tsx new file mode 100644 index 00000000..dd58727b --- /dev/null +++ b/app/(protected)/project/[project_id]/prompts/[prompt_id]/page.tsx @@ -0,0 +1,247 @@ +"use client"; +import CreatePromptDialog from "@/components/shared/create-prompt-dialog"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; +import { Prompt } from "@prisma/client"; +import CodeEditor from "@uiw/react-textarea-code-editor"; +import { ChevronLeft } from "lucide-react"; +import { useParams, useRouter } from "next/navigation"; +import { useState } from "react"; +import { useQuery, useQueryClient } from "react-query"; +import { toast } from "sonner"; + +export default function Prompt() { + const promptsetId = useParams()?.prompt_id as string; + const router = useRouter(); + const [prompts, setPrompts] = useState([]); + const [selectedPrompt, setSelectedPrompt] = useState(); + const [live, setLive] = useState(false); + const queryClient = useQueryClient(); + + const { isLoading: promptsLoading, error: promptsError } = useQuery({ + queryKey: ["fetch-prompts-query", promptsetId], + queryFn: async () => { + const response = await fetch( + `/api/promptset?promptset_id=${promptsetId}` + ); + if (!response.ok) { + const error = await response.json(); + throw new Error(error?.message || "Failed to fetch tests"); + } + const result = await response.json(); + setPrompts(result?.promptsets?.prompts || []); + if (result?.promptsets?.prompts.length > 0 && !selectedPrompt) { + setSelectedPrompt(result?.promptsets?.prompts[0]); + setLive(result?.promptsets?.prompts[0].live); + } + return result; + }, + onError: (error) => { + toast.error("Failed to fetch prompts", { + description: error instanceof Error ? error.message : String(error), + }); + }, + }); + + if (promptsLoading) return ; + + if (!selectedPrompt) + return ( +
+ +
+

Create your first prompt

+

+ Start by creating the first version of your prompt. Once created, + you can test it in the playground with different models and model + settings and continue to iterate and add more versions to the + prompt. +

+ +
+
+ ); + else + return ( +
+
+ + {prompts.length > 0 ? ( + + ) : ( + + )} +
+
+
+ {prompts.map((prompt: Prompt, i) => ( +
{ + setSelectedPrompt(prompt); + setLive(prompt.live); + }} + className={cn( + "flex gap-4 items-start w-full rounded-md p-2 hover:bg-muted cursor-pointer", + selectedPrompt.id === prompt.id ? "bg-muted" : "" + )} + key={prompt.id} + > +
+
+ v{prompt.version} +
+ +
+
+ {prompt.live && ( +

+ Live +

+ )} +

+ {prompt.note || `Version ${prompt.version}`} +

+
+
+ ))} +
+
+
+ +

+ {selectedPrompt.value} +

+
+
+ +
+ {selectedPrompt.variables.map((variable: string) => ( + + {variable} + + ))} +
+
+
+ +

+ {selectedPrompt.model ?? "None"} +

+
+
+ + +
+
+ +
+ { + setLive(checked as boolean); + try { + const payload = { + ...selectedPrompt, + live: checked, + }; + await fetch("/api/prompt", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + await queryClient.invalidateQueries({ + queryKey: ["fetch-prompts-query", promptsetId], + }); + toast.success( + checked + ? "This prompt is now live" + : "This prompt is no longer live. Make sure to make another prompt live" + ); + } catch (error) { + toast.error("Failed to make prompt live", { + description: + error instanceof Error + ? error.message + : String(error), + }); + } + }} + /> +

+ Make this version of the prompt live +

+
+
+
+
+
+ ); +} + +function PageLoading() { + return ( +
+
+ + +
+
+ + +
+
+ ); +} diff --git a/app/(protected)/project/[project_id]/prompts/page-client.tsx b/app/(protected)/project/[project_id]/prompts/page-client.tsx index 94815450..8b4eafcf 100644 --- a/app/(protected)/project/[project_id]/prompts/page-client.tsx +++ b/app/(protected)/project/[project_id]/prompts/page-client.tsx @@ -1,6 +1,5 @@ "use client"; -import { AddtoPromptset } from "@/components/shared/add-to-promptset"; import { Spinner } from "@/components/shared/spinner"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -8,7 +7,6 @@ import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { PAGE_SIZE } from "@/lib/constants"; import { extractSystemPromptFromLlmInputs } from "@/lib/utils"; -import { CheckCircledIcon } from "@radix-ui/react-icons"; import { ChevronDown, ChevronRight, RabbitIcon } from "lucide-react"; import { useParams } from "next/navigation"; import { useState } from "react"; @@ -42,7 +40,7 @@ export default function PageClient({ email }: { email: string }) { queryKey: [`fetch-prompts-${projectId}-query`], queryFn: async () => { const response = await fetch( - `/api/prompt?projectId=${projectId}&page=${page}&pageSize=${PAGE_SIZE}` + `/api/span-prompt?projectId=${projectId}&page=${page}&pageSize=${PAGE_SIZE}` ); if (!response.ok) { const error = await response.json(); @@ -130,9 +128,6 @@ export default function PageClient({ email }: { email: string }) { return (
-
- -

These prompts are automatically captured from your traces. The accuracy of these prompts are calculated based on the evaluation done @@ -145,7 +140,6 @@ export default function PageClient({ email }: { email: string }) {

Interactions

Prompt

Accuracy

-

Added to Dataset

{dedupedPrompts.map((prompt: any, i: number) => { return ( @@ -187,26 +181,6 @@ const PromptRow = ({ }) => { const [collapsed, setCollapsed] = useState(true); const [accuracy, setAccuracy] = useState(0); - const [addedToPromptset, setAddedToPromptset] = useState(false); - - useQuery({ - queryKey: [`fetch-promptdata-query-${prompt.span_id}`], - queryFn: async () => { - const response = await fetch(`/api/promptdata?spanId=${prompt.span_id}`); - if (!response.ok) { - const error = await response.json(); - throw new Error(error?.message || "Failed to fetch prompt data"); - } - const result = await response.json(); - setAddedToPromptset(result.data.length > 0); - return result; - }, - onError: (error) => { - toast.error("Failed to fetch prompt data", { - description: error instanceof Error ? error.message : String(error), - }); - }, - }); // Get the evaluation for this prompt's content const attributes = prompt.attributes ? JSON.parse(prompt.attributes) : {}; @@ -302,11 +276,6 @@ const PromptRow = ({

{accuracy?.toFixed(2)}%

- {addedToPromptset ? ( - - ) : ( - "" - )} {!collapsed && (
@@ -325,9 +294,6 @@ const PromptRow = ({ function PageLoading() { return (
-
- -

These prompts are automatically captured from your traces. The accuracy of these prompts are calculated based on the evaluation done in the diff --git a/app/(protected)/project/[project_id]/prompts/page.tsx b/app/(protected)/project/[project_id]/prompts/page.tsx index f6698bb8..0f0d9dca 100644 --- a/app/(protected)/project/[project_id]/prompts/page.tsx +++ b/app/(protected)/project/[project_id]/prompts/page.tsx @@ -2,7 +2,7 @@ import { authOptions } from "@/lib/auth/options"; import { Metadata } from "next"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; -import PageClient from "./page-client"; +import PromptManagement from "./prompt-management"; export const metadata: Metadata = { title: "Langtrace | Prompts", @@ -18,7 +18,7 @@ export default async function Page() { return ( <> - + ); } diff --git a/components/project/dataset/prompt-set.tsx b/app/(protected)/project/[project_id]/prompts/prompt-management.tsx similarity index 63% rename from components/project/dataset/prompt-set.tsx rename to app/(protected)/project/[project_id]/prompts/prompt-management.tsx index c4bccf9b..fb5a9706 100644 --- a/components/project/dataset/prompt-set.tsx +++ b/app/(protected)/project/[project_id]/prompts/prompt-management.tsx @@ -1,3 +1,7 @@ +"use client"; + +import { CreatePromptset } from "@/components/project/dataset/create"; +import { EditPromptSet } from "@/components/project/dataset/edit"; import CardLoading from "@/components/shared/card-skeleton"; import { Card, @@ -11,10 +15,8 @@ import Link from "next/link"; import { useParams } from "next/navigation"; import { useQuery } from "react-query"; import { toast } from "sonner"; -import { CreatePromptset } from "./create"; -import { EditPromptSet } from "./edit"; -export default function PromptSet({ email }: { email: string }) { +export default function PromptManagement({ email }: { email: string }) { const projectId = useParams()?.project_id as string; const { @@ -22,18 +24,18 @@ export default function PromptSet({ email }: { email: string }) { isLoading: promptsetsLoading, error: promptsetsError, } = useQuery({ - queryKey: [`fetch-promptsets-stats-${projectId}-query`], + queryKey: ["fetch-promptsets-query", projectId], queryFn: async () => { - const response = await fetch(`/api/stats/promptset?id=${projectId}`); + const response = await fetch(`/api/promptset?id=${projectId}`); if (!response.ok) { const error = await response.json(); - throw new Error(error?.message || "Failed to fetch promptsets"); + throw new Error(error?.message || "Failed to fetch prompt sets"); } const result = await response.json(); return result; }, onError: (error) => { - toast.error("Failed to fetch promptsets", { + toast.error("Failed to fetch prompt sets", { description: error instanceof Error ? error.message : String(error), }); }, @@ -43,47 +45,46 @@ export default function PromptSet({ email }: { email: string }) { return ; } else if (promptsetsError) { return ( -

+

Failed to fetch promptsets

); } else { return ( -
+
- {promptsets?.result?.length === 0 && ( + {promptsets?.promptsets?.length === 0 && (

- Get started by creating your first prompt set. + Get started by creating your first prompt registry.

- Prompt Sets help you categorize and manage a set of prompts. Say - you would like to group the prompts that give an accuracy of 90% - of more. You can use the eval tab to add new records to any of - the prompt sets. + A Prompt registry is a collection of versioned prompts all + related to a single prompt. You can create a prompt registry, + add a prompt and continue to update and version the prompt. You + can also access the prompt using the API and use it in your + application.

)} - {promptsets?.result?.map((promptset: any, i: number) => ( + {promptsets?.promptsets?.map((promptset: any, i: number) => (
- +
- + - {promptset?.promptset?.name} + {promptset?.name}
-

{promptset?.promptset?.description}

+

{promptset?.description}

- {promptset?.totalPrompts || 0} prompts + {promptset?._count?.Prompt || 0} versions

@@ -100,13 +101,13 @@ export default function PromptSet({ email }: { email: string }) { function PageLoading() { return ( -
+
{Array.from({ length: 3 }).map((_, index) => ( diff --git a/app/(protected)/projects/page-client.tsx b/app/(protected)/projects/page-client.tsx index 1bad0cd3..fb30451f 100644 --- a/app/(protected)/projects/page-client.tsx +++ b/app/(protected)/projects/page-client.tsx @@ -178,13 +178,7 @@ function ProjectCard({ )}
- + @@ -225,7 +219,7 @@ function ProjectCard({

-

Prompt sets

+

Prompts

{projectStats?.totalPromptsets || 0}

diff --git a/app/(protected)/settings/keys/page-client.tsx b/app/(protected)/settings/keys/page-client.tsx new file mode 100644 index 00000000..aaa4f748 --- /dev/null +++ b/app/(protected)/settings/keys/page-client.tsx @@ -0,0 +1,89 @@ +"use client"; + +import AddApiKey from "@/components/shared/add-api-key"; +import CodeBlock from "@/components/shared/code-block"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { LLM_VENDORS, LLM_VENDOR_APIS } from "@/lib/constants"; +import { Trash2Icon } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +export default function ApiKeys() { + const [busy, setBusy] = useState(false); + const [vendorKeys, setVendorKeys] = useState([]); + + useEffect(() => { + if (typeof window === "undefined") return; + const keys = LLM_VENDORS.map((vendor) => { + const keyName = LLM_VENDOR_APIS.find( + (api) => api.label.toUpperCase() === vendor.value.toUpperCase() + ); + if (!keyName) return null; + const key = window.localStorage.getItem(keyName.value.toUpperCase()); + if (!key) return null; + return { value: keyName.value.toUpperCase(), label: vendor.label, key }; + }); + if (keys.length === 0) return setVendorKeys([]); + // filter out null values + setVendorKeys(keys.filter(Boolean)); + }, [busy]); + + return ( +
+
+ setBusy(!busy)} /> +
+ + + API Keys + Add/Manage your API Keys +

+ {" "} + Note: We do not store your API keys and we use the browser store to + save it ONLY for the session. Clearing the browser cache will remove + the keys. +

+
+ +
+ {vendorKeys.length === 0 && ( +

+ There are no API keys stored +

+ )} + {vendorKeys.map((vendor) => { + return ( +
+ +
+ + +
+
+ ); + })} +
+
+
+
+ ); +} diff --git a/app/(protected)/settings/keys/page.tsx b/app/(protected)/settings/keys/page.tsx new file mode 100644 index 00000000..b5fc35a9 --- /dev/null +++ b/app/(protected)/settings/keys/page.tsx @@ -0,0 +1,23 @@ +import { authOptions } from "@/lib/auth/options"; +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import PageClient from "./page-client"; + +export default async function Page() { + const session = await getServerSession(authOptions); + if (!session || !session.user) { + redirect("/login"); + } + const email = session?.user?.email as string; + + const resp = await fetch( + `${process.env.NEXTAUTH_URL_INTERNAL}/api/user?email=${email}` + ); + const user = await resp.json(); + + return ( + <> + + + ); +} diff --git a/app/api/chat/anthropic/route.ts b/app/api/chat/anthropic/route.ts new file mode 100644 index 00000000..e3625519 --- /dev/null +++ b/app/api/chat/anthropic/route.ts @@ -0,0 +1,37 @@ +import Anthropic from "@anthropic-ai/sdk"; +import { AnthropicStream, StreamingTextResponse } from "ai"; +import { NextResponse } from "next/server"; + +export async function POST(req: Request) { + try { + const data = await req.json(); + const isStream = data.stream; + const apiKey = data.apiKey; + + // Create an Anthropic API client + const anthropic = new Anthropic({ + apiKey: apiKey, + }); + + // remove apiKey from the body + delete data.apiKey; + + // Ask OpenAI for a streaming chat completion given the prompt + const response = await anthropic.messages.create({ + ...data, + }); + + // Convert the response into a friendly text-stream + if (isStream) { + const stream = AnthropicStream(response as any); + return new StreamingTextResponse(stream); + } + + return NextResponse.json(response); + } catch (error: any) { + return NextResponse.json({ + error: error?.message || "Something went wrong", + status: error?.status || error?.message.includes("apiKey") ? 401 : 500, + }); + } +} diff --git a/app/api/chat/cohere/route.ts b/app/api/chat/cohere/route.ts new file mode 100644 index 00000000..33f0b806 --- /dev/null +++ b/app/api/chat/cohere/route.ts @@ -0,0 +1,40 @@ +import { CohereStream, StreamingTextResponse } from "ai"; +import { CohereClient } from "cohere-ai"; +import { NextResponse } from "next/server"; + +export async function POST(req: Request) { + try { + const data = await req.json(); + const isStream = data.stream; + const apiKey = data.apiKey; + + // Create an Cohere API client + const cohere = new CohereClient({ + token: apiKey, + }); + + // remove apiKey from the body + delete data.apiKey; + + // Ask cohere for a streaming chat completion given the prompt + const response = await cohere.chat({ + ...data, + }); + + // Convert the response into a friendly text-stream + if (isStream) { + const stream = CohereStream(response as any); + return new StreamingTextResponse(stream); + } + + return NextResponse.json(response); + } catch (error: any) { + return NextResponse.json({ + error: error?.message || "Something went wrong", + status: + error?.status || error?.message.includes("Status code: 401") + ? 401 + : 500, + }); + } +} diff --git a/app/api/chat/groq/route.ts b/app/api/chat/groq/route.ts new file mode 100644 index 00000000..d175dd62 --- /dev/null +++ b/app/api/chat/groq/route.ts @@ -0,0 +1,37 @@ +import { OpenAIStream, StreamingTextResponse } from "ai"; +import Groq from "groq-sdk"; +import { NextResponse } from "next/server"; + +export async function POST(req: Request) { + try { + const data = await req.json(); + const isStream = data.stream; + const apiKey = data.apiKey; + + // Create an Groq API client (that's edge friendly!) + const groq = new Groq({ + apiKey: apiKey, + }); + + // remove apiKey from the body + delete data.apiKey; + + // Ask Groq for a streaming chat completion given the prompt + const response = await groq.chat.completions.create({ + ...data, + }); + + // Convert the response into a friendly text-stream + if (isStream) { + const stream = OpenAIStream(response as any); + return new StreamingTextResponse(stream); + } + + return NextResponse.json(response); + } catch (error: any) { + return NextResponse.json({ + error: error?.message || "Something went wrong", + status: error?.status || 500, + }); + } +} diff --git a/app/api/chat/openai/route.ts b/app/api/chat/openai/route.ts new file mode 100644 index 00000000..8eeb3293 --- /dev/null +++ b/app/api/chat/openai/route.ts @@ -0,0 +1,37 @@ +import { OpenAIStream, StreamingTextResponse } from "ai"; +import { NextResponse } from "next/server"; +import OpenAI from "openai"; + +export async function POST(req: Request) { + try { + const data = await req.json(); + const isStream = data.stream; + const apiKey = data.apiKey; + + // Create an OpenAI API client (that's edge friendly!) + const openai = new OpenAI({ + apiKey: apiKey, + }); + + // remove apiKey from the body + delete data.apiKey; + + // Ask OpenAI for a streaming chat completion given the prompt + const response = await openai.chat.completions.create({ + ...data, + }); + + // Convert the response into a friendly text-stream + if (isStream) { + const stream = OpenAIStream(response as any); + return new StreamingTextResponse(stream); + } + + return NextResponse.json(response); + } catch (error: any) { + return NextResponse.json({ + error: error?.message || "Something went wrong", + status: error?.status || 500, + }); + } +} diff --git a/app/api/prompt/route.ts b/app/api/prompt/route.ts index 3160b72d..8bd8ccd0 100644 --- a/app/api/prompt/route.ts +++ b/app/api/prompt/route.ts @@ -1,49 +1,159 @@ import { authOptions } from "@/lib/auth/options"; import prisma from "@/lib/prisma"; -import { TraceService } from "@/lib/services/trace_service"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; import { NextRequest, NextResponse } from "next/server"; export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session || !session.user) { redirect("/login"); } - try { - const projectId = req.nextUrl.searchParams.get("projectId") as string; - const page = - (req.nextUrl.searchParams.get("page") as unknown as number) || 1; - const pageSize = - (req.nextUrl.searchParams.get("pageSize") as unknown as number) || 10; - - if (!projectId) { - return NextResponse.json( - { message: "Please provide a projectId or spanId" }, - { status: 400 } - ); - } - const traceService = new TraceService(); - const prompts = await traceService.GetSpansWithAttribute( - "llm.prompts", - projectId, - page, - pageSize - ); + const id = req.nextUrl.searchParams.get("id") as string; + if (!id) { return NextResponse.json( - { prompts }, { - status: 200, - } + error: "No prompt id provided", + }, + { status: 404 } ); - } catch (error) { - return NextResponse.json(JSON.stringify({ message: error }), { - status: 500, + } + + const result = await prisma.prompt.findFirst({ + where: { + id, + }, + }); + + return NextResponse.json({ + data: result, + }); +} + +export async function POST(req: NextRequest) { + const session = await getServerSession(authOptions); + if (!session || !session.user) { + redirect("/login"); + } + + const data = await req.json(); + const { + value, + variables, + model, + modelSettings, + version, + live, + note, + promptsetId, + } = data; + const dataToAdd: any = { + value, + variables, + model, + modelSettings, + version, + live, + note, + promptsetId, + }; + + if (data.spanId) { + dataToAdd.spanId = data.spanId; + } + + if (live) { + const existingLivePrompt = await prisma.prompt.findFirst({ + where: { + live: true, + }, + }); + + if (existingLivePrompt) { + await prisma.prompt.update({ + where: { + id: existingLivePrompt.id, + }, + data: { + live: false, + }, + }); + } + } + + const result = await prisma.prompt.create({ + data: dataToAdd, + }); + + return NextResponse.json({ + data: result, + }); +} + +export async function PUT(req: NextRequest) { + const session = await getServerSession(authOptions); + if (!session || !session.user) { + redirect("/login"); + } + + const data = await req.json(); + const { + id, + value, + variables, + model, + modelSettings, + version, + live, + note, + promptsetId, + } = data; + const dataToUpdate: any = { + value, + variables, + model, + modelSettings, + version, + live, + note, + promptsetId, + }; + + if (data.spanId) { + dataToUpdate.spanId = data.spanId; + } + + if (live) { + const existingLivePrompt = await prisma.prompt.findFirst({ + where: { + live: true, + }, }); + + if (existingLivePrompt) { + await prisma.prompt.update({ + where: { + id: existingLivePrompt.id, + }, + data: { + live: false, + }, + }); + } } + + const result = await prisma.prompt.update({ + where: { + id, + }, + data: dataToUpdate, + }); + + return NextResponse.json({ + data: result, + }); } export async function DELETE(req: NextRequest) { diff --git a/app/api/promptdata/route.ts b/app/api/promptdata/route.ts deleted file mode 100644 index ff41133d..00000000 --- a/app/api/promptdata/route.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { authOptions } from "@/lib/auth/options"; -import prisma from "@/lib/prisma"; -import { getServerSession } from "next-auth"; -import { redirect } from "next/navigation"; -import { NextRequest, NextResponse } from "next/server"; - -export async function GET(req: NextRequest) { - try { - const session = await getServerSession(authOptions); - if (!session || !session.user) { - redirect("/login"); - } - - const id = req.nextUrl.searchParams.get("id") as string; - const spanId = req.nextUrl.searchParams.get("spanId") as string; - if (!spanId && !id) { - return NextResponse.json( - { - message: "No span id or prompt id provided", - }, - { status: 404 } - ); - } - - if (id) { - const result = await prisma.prompt.findFirst({ - where: { - id, - }, - }); - - return NextResponse.json({ - data: result, - }); - } - - if (spanId) { - const result = await prisma.prompt.findMany({ - where: { - spanId, - }, - }); - - return NextResponse.json({ - data: result, - }); - } - } catch (error) { - return NextResponse.json( - { - message: "Internal server error", - }, - { status: 500 } - ); - } -} - -export async function POST(req: NextRequest) { - const session = await getServerSession(authOptions); - if (!session || !session.user) { - redirect("/login"); - } - - const data = await req.json(); - const { datas, promptsetId } = data; - - const result = await prisma.prompt.createMany({ - data: datas.map((data: any) => { - return { - value: data.value, - note: data.note || "", - spanId: data.spanId || "", - promptsetId, - }; - }), - }); - - return NextResponse.json({ - data: result, - }); -} - -export async function PUT(req: NextRequest) { - const session = await getServerSession(authOptions); - if (!session || !session.user) { - redirect("/login"); - } - - const data = await req.json(); - const { id, value, note } = data; - - const result = await prisma.prompt.update({ - where: { - id, - }, - data: { - value, - note, - }, - }); - - return NextResponse.json({ - data: result, - }); -} - -export async function DELETE(req: NextRequest) { - const session = await getServerSession(authOptions); - if (!session || !session.user) { - redirect("/login"); - } - - const data = await req.json(); - const { id } = data; - - const result = await prisma.prompt.delete({ - where: { - id, - }, - }); - - return NextResponse.json({}); -} diff --git a/app/api/promptset/route.ts b/app/api/promptset/route.ts index e9ff2060..404d806f 100644 --- a/app/api/promptset/route.ts +++ b/app/api/promptset/route.ts @@ -33,9 +33,6 @@ export async function GET(req: NextRequest) { where: { id: promptsetId, }, - include: { - Prompt: true, - }, }); if (!promptset) { @@ -77,7 +74,7 @@ export async function GET(req: NextRequest) { // Combine dataset with its related, ordered Data const promptsetWithOrderedData = { ...promptset, - Prompt: relatedPrompt, + prompts: relatedPrompt, }; return NextResponse.json({ @@ -106,12 +103,16 @@ export async function GET(req: NextRequest) { where: { projectId: id, }, - include: { - Prompt: true, - }, orderBy: { createdAt: "desc", }, + include: { + _count: { + select: { + Prompt: true, + }, + }, + }, }); return NextResponse.json({ diff --git a/app/api/span-prompt/route.ts b/app/api/span-prompt/route.ts new file mode 100644 index 00000000..3160b72d --- /dev/null +++ b/app/api/span-prompt/route.ts @@ -0,0 +1,66 @@ +import { authOptions } from "@/lib/auth/options"; +import prisma from "@/lib/prisma"; +import { TraceService } from "@/lib/services/trace_service"; +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(req: NextRequest) { + const session = await getServerSession(authOptions); + + if (!session || !session.user) { + redirect("/login"); + } + try { + const projectId = req.nextUrl.searchParams.get("projectId") as string; + const page = + (req.nextUrl.searchParams.get("page") as unknown as number) || 1; + const pageSize = + (req.nextUrl.searchParams.get("pageSize") as unknown as number) || 10; + + if (!projectId) { + return NextResponse.json( + { message: "Please provide a projectId or spanId" }, + { status: 400 } + ); + } + + const traceService = new TraceService(); + const prompts = await traceService.GetSpansWithAttribute( + "llm.prompts", + projectId, + page, + pageSize + ); + + return NextResponse.json( + { prompts }, + { + status: 200, + } + ); + } catch (error) { + return NextResponse.json(JSON.stringify({ message: error }), { + status: 500, + }); + } +} + +export async function DELETE(req: NextRequest) { + const session = await getServerSession(authOptions); + + if (!session || !session.user) { + redirect("/login"); + } + + const data = await req.json(); + const { id } = data; + + const prompt = await prisma.prompt.delete({ + where: { + id, + }, + }); + + return NextResponse.json({}); +} diff --git a/components/evaluations/evaluation-row.tsx b/components/evaluations/evaluation-row.tsx index f303f5cc..d54fa68c 100644 --- a/components/evaluations/evaluation-row.tsx +++ b/components/evaluations/evaluation-row.tsx @@ -1,5 +1,3 @@ -"use client"; - import { HoverCell } from "@/components/shared/hover-cell"; import { LLMView } from "@/components/shared/llm-view"; import { Button } from "@/components/ui/button"; @@ -265,8 +263,8 @@ export default function EvaluationRow({
{!collapsed && ( )} diff --git a/components/evaluations/evaluation-table.tsx b/components/evaluations/evaluation-table.tsx index 2f4d536e..b48b367b 100644 --- a/components/evaluations/evaluation-table.tsx +++ b/components/evaluations/evaluation-table.tsx @@ -1,5 +1,3 @@ -"use client"; - import { TestSetupInstructions } from "@/components/shared/setup-instructions"; import { Spinner } from "@/components/shared/spinner"; import { PAGE_SIZE } from "@/lib/constants"; diff --git a/components/playground/chat-handlers.ts b/components/playground/chat-handlers.ts new file mode 100644 index 00000000..6c498064 --- /dev/null +++ b/components/playground/chat-handlers.ts @@ -0,0 +1,301 @@ +import { + AnthropicChatInterface, + CohereChatInterface, + GroqChatInterface, + OpenAIChatInterface, +} from "@/lib/types/playground_types"; + +export async function openAIHandler( + llm: OpenAIChatInterface, + apiKey: string +): Promise { + const body: any = {}; + if (llm.settings.messages.length > 0) { + body.messages = llm.settings.messages.map((m) => { + return { content: m.content, role: m.role }; + }); + } + if (llm.settings.model) { + body.model = llm.settings.model; + } + if (llm.settings.temperature) { + body.temperature = llm.settings.temperature; + } + if (llm.settings.maxTokens) { + body.max_tokens = llm.settings.maxTokens; + } + if (llm.settings.n) { + body.n = llm.settings.n; + } + if (llm.settings.stop) { + body.stop = llm.settings.stop; + } + if (llm.settings.frequencyPenalty) { + body.frequency_penalty = llm.settings.frequencyPenalty; + } + if (llm.settings.presencePenalty) { + body.presence_penalty = llm.settings.presencePenalty; + } + if (llm.settings.logProbs) { + body.logprobs = llm.settings.logProbs; + } + if (llm.settings.topLogProbs) { + body.top_logprobs = llm.settings.topLogProbs; + } + if (llm.settings.logitBias !== undefined) { + body.logit_bias = llm.settings.logitBias; + } + if (llm.settings.responseFormat) { + body.response_format = llm.settings.responseFormat; + } + if (llm.settings.seed) { + body.seed = llm.settings.seed; + } + if (llm.settings.stream !== undefined) { + body.stream = llm.settings.stream; + } + if (llm.settings.topP) { + body.top_p = llm.settings.topP; + } + if (llm.settings.tools && llm.settings.tools.length > 0) { + body.tools = llm.settings.tools; + } + if (llm.settings.toolChoice) { + body.tool_choice = llm.settings.toolChoice; + } + if (llm.settings.user) { + body.user = llm.settings.user; + } + + // Get the API key from the browser store + body.apiKey = apiKey; + + const response = await fetch("/api/chat/openai", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + return response; +} + +export async function anthropicHandler( + llm: AnthropicChatInterface, + apiKey: string +): Promise { + const body: any = {}; + if (llm.settings.messages.length > 0) { + body.messages = llm.settings.messages.map((m) => { + return { content: m.content, role: m.role }; + }); + } + if (llm.settings.model) { + body.model = llm.settings.model; + } + if (llm.settings.temperature) { + body.temperature = llm.settings.temperature; + } + if (llm.settings.maxTokens) { + body.max_tokens = llm.settings.maxTokens; + } + if (llm.settings.stream !== undefined) { + body.stream = llm.settings.stream; + } + if (llm.settings.topP) { + body.top_p = llm.settings.topP; + } + if (llm.settings.tools && llm.settings.tools.length > 0) { + body.tools = llm.settings.tools; + } + if (llm.settings.topK) { + body.top_k = llm.settings.topK; + } + if (llm.settings.metadata) { + body.metadata = llm.settings.metadata; + } + if (llm.settings.system) { + body.system = llm.settings.system; + } + + // Get the API key from the browser store + body.apiKey = apiKey; + + const response = await fetch("/api/chat/anthropic", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + return response; +} + +export async function cohereHandler( + llm: CohereChatInterface, + apiKey: string +): Promise { + const body: any = {}; + if (llm.settings.messages.length > 0) { + body.message = + llm.settings.messages[llm.settings.messages.length - 1].content; + body.chat_history = llm.settings.messages.map((m, i) => { + if (i === llm.settings.messages.length - 1) return null; + return { message: m.content, role: m.role }; + }); + } + // remove null values + body.chat_history = body.chat_history.filter(Boolean); + + if (llm.settings.model) { + body.model = llm.settings.model; + } + if (llm.settings.temperature) { + body.temperature = llm.settings.temperature; + } + if (llm.settings.maxTokens) { + body.max_tokens = llm.settings.maxTokens; + } + if (llm.settings.maxInputTokens) { + body.max_input_tokens = llm.settings.maxInputTokens; + } + if (llm.settings.stream !== undefined) { + body.stream = llm.settings.stream; + } + if (llm.settings.preamble) { + body.preamble = llm.settings.preamble; + } + if (llm.settings.conversationId) { + body.conversation_id = llm.settings.conversationId; + } + if (llm.settings.promptTruncation) { + body.prompt_truncation = llm.settings.promptTruncation; + } + if (llm.settings.connectors) { + body.connectors = llm.settings.connectors; + } + if (llm.settings.searchQueriesOnly) { + body.search_queries_only = llm.settings.searchQueriesOnly; + } + if (llm.settings.documents) { + body.documents = llm.settings.documents; + } + if (llm.settings.citationQuality) { + body.citation_quality = llm.settings.citationQuality; + } + if (llm.settings.k) { + body.k = llm.settings.k; + } + if (llm.settings.p) { + body.p = llm.settings.p; + } + if (llm.settings.seed) { + body.seed = llm.settings.seed; + } + if (llm.settings.stopSequences) { + body.stop_sequences = llm.settings.stopSequences; + } + if (llm.settings.frequencyPenalty) { + body.frequency_penalty = llm.settings.frequencyPenalty; + } + if (llm.settings.presencePenalty) { + body.presence_penalty = llm.settings.presencePenalty; + } + if (llm.settings.tools && llm.settings.tools.length > 0) { + body.tools = llm.settings.tools; + } + if (llm.settings.toolResults) { + body.tool_results = llm.settings.toolResults; + } + + // Get the API key from the browser store + body.apiKey = apiKey; + + const response = await fetch("/api/chat/cohere", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + return response; +} + +export async function groqHandler( + llm: GroqChatInterface, + apiKey: string +): Promise { + const body: any = {}; + if (llm.settings.messages.length > 0) { + body.messages = llm.settings.messages.map((m) => { + return { content: m.content, role: m.role }; + }); + } + if (llm.settings.model) { + body.model = llm.settings.model; + } + if (llm.settings.temperature) { + body.temperature = llm.settings.temperature; + } + if (llm.settings.maxTokens) { + body.max_tokens = llm.settings.maxTokens; + } + if (llm.settings.n) { + body.n = llm.settings.n; + } + if (llm.settings.stop) { + body.stop = llm.settings.stop; + } + if (llm.settings.frequencyPenalty) { + body.frequency_penalty = llm.settings.frequencyPenalty; + } + if (llm.settings.presencePenalty) { + body.presence_penalty = llm.settings.presencePenalty; + } + if (llm.settings.logProbs) { + body.logprobs = llm.settings.logProbs; + } + if (llm.settings.topLogProbs) { + body.top_logprobs = llm.settings.topLogProbs; + } + if (llm.settings.logitBias !== undefined) { + body.logit_bias = llm.settings.logitBias; + } + if (llm.settings.responseFormat) { + body.response_format = llm.settings.responseFormat; + } + if (llm.settings.seed) { + body.seed = llm.settings.seed; + } + if (llm.settings.stream !== undefined) { + body.stream = llm.settings.stream; + } + if (llm.settings.topP) { + body.top_p = llm.settings.topP; + } + if (llm.settings.tools && llm.settings.tools.length > 0) { + body.tools = llm.settings.tools; + } + if (llm.settings.toolChoice) { + body.tool_choice = llm.settings.toolChoice; + } + if (llm.settings.user) { + body.user = llm.settings.user; + } + + // Get the API key from the browser store + body.apiKey = apiKey; + + const response = await fetch("/api/chat/groq", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + return response; +} diff --git a/components/playground/common.tsx b/components/playground/common.tsx new file mode 100644 index 00000000..e68fef71 --- /dev/null +++ b/components/playground/common.tsx @@ -0,0 +1,169 @@ +import LLMPicker from "@/components/shared/llm-picker"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { + CohereAIRole, + Conversation, + OpenAIRole, +} from "@/lib/types/playground_types"; +import { cn } from "@/lib/utils"; +import { MinusCircleIcon, PlusIcon } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +export function RoleBadge({ + role, + onSelect, +}: { + role: OpenAIRole | CohereAIRole; + onSelect: () => void; +}) { + return ( + + ); +} + +export function ExpandingTextArea({ + value, + onChange, + setFocusing, +}: { + value: string; + onChange: any; + setFocusing?: any; +}) { + const textAreaRef = useRef(null); + + const handleClickOutside = (event: any) => { + if (textAreaRef.current && !textAreaRef.current.contains(event.target)) { + setFocusing(false); + } + }; + + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + const handleChange = (event: any) => { + const textarea = event.target; + onChange(textarea.value); + + textarea.style.height = "auto"; + textarea.style.height = `${textarea.scrollHeight}px`; + }; + + return ( +