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 ( +