Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec59436522 | ||
|
|
53a3d58a2f | ||
|
|
c1162a3a8b | ||
|
|
43d8c4ec99 | ||
|
|
3a88315ac5 | ||
|
|
e06b96a5ee | ||
|
|
307f54ef8a | ||
|
|
196dc1bf00 | ||
|
|
ee2c0fb27b | ||
|
|
27623de650 | ||
|
|
61c8f6c269 | ||
|
|
cc9e21ccb3 | ||
|
|
078ceb7ff1 | ||
|
|
9436c33025 | ||
|
|
88b3e1bea6 | ||
|
|
da042d8284 | ||
|
|
50264701f6 | ||
|
|
3ee8a2e026 | ||
|
|
ed04357815 | ||
|
|
fa26275634 | ||
|
|
7587c75459 | ||
|
|
a956d0115c | ||
|
|
cb82835489 | ||
|
|
d1c1e60eca | ||
|
|
81b612273a | ||
|
|
2c1abf2f6e | ||
|
|
db1a662f7f | ||
|
|
1b0e3de709 | ||
|
|
be8aec2f8f | ||
|
|
adc01014c0 | ||
|
|
215af995fb | ||
|
|
74ff237145 | ||
|
|
5c2ae0e678 | ||
|
|
a6d8ce8c96 |
+1
-1
@@ -47,5 +47,5 @@ REDIS_PORT=6379
|
||||
REDIS_AUTH="myredissecret"
|
||||
|
||||
LANGFUSE_WORKER_PASSWORD=mybasicauthsecret
|
||||
# openssl rand -base64 32 used only here
|
||||
# openssl rand -hex 32 used only here
|
||||
ENCRYPTION_KEY=6c16874e5c0f0cc74ddec00425fa99fbe9ffbe412b7d5d906a4b00005df91403
|
||||
@@ -58,6 +58,11 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
# AUTH_AUTH0_CLIENT_SECRET=
|
||||
# AUTH_AUTH0_ISSUER=
|
||||
# AUTH_AUTH0_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_COGNITO_CLIENT_ID=
|
||||
# AUTH_COGNITO_CLIENT_SECRET=
|
||||
# AUTH_COGNITO_ISSUER=
|
||||
# AUTH_COGNITO_ALLOW_ACCOUNT_LINKING=false
|
||||
|
||||
|
||||
# Transactional email, optional
|
||||
# Defines the email address to use as the from address.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
name: Deploy to worker
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Github tag
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Setup porter
|
||||
uses: porter-dev/setup-porter@v0.1.0
|
||||
- name: Deploy stack
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "3959"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: d2bb23e4-1d77-48f2-a113-383d076959b3
|
||||
PORTER_HOST: https://dashboard.getporter.dev
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: worker
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_3959 }}
|
||||
@@ -106,22 +106,26 @@ See the [→ Quickstart](https://langfuse.com/docs/get-started) to integrate Lan
|
||||
|
||||
### Integrations
|
||||
|
||||
| Integration | Supports | Description |
|
||||
| ---------------------------------------------------------------- | ------------- | ----------------------------------------------------------- |
|
||||
| [**SDK** - _recommended_](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
|
||||
| [OpenAI SDK](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | Automated instrumentation of OpenAI SDK. |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Instrumentation via Langchain callbacks. |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index) | Python | Automated instrumentation via LlamaIndex callback system. |
|
||||
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
|
||||
| Integration | Supports | Description |
|
||||
| -------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [SDK](/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
|
||||
| [OpenAI](/docs/integrations/openai) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
|
||||
| [Langchain](/docs/integrations/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
|
||||
| [LlamaIndex](/docs/integrations/llama-index/get-started) | Python | Automated instrumentation via LlamaIndex callback system. |
|
||||
| [Haystack](/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
|
||||
| [LiteLLM](/docs/integrations/litellm) | Python, JS/TS (proxy only) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
|
||||
| [API](/docs/api) | | Directly call the public API. OpenAPI spec available. |
|
||||
|
||||
External projects/packages that integrate with Langfuse:
|
||||
Packages that integrate with Langfuse:
|
||||
|
||||
| Name | Description |
|
||||
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | JS/TS no-code builder for customized LLM flows. |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
|
||||
| [Superagent](https://langfuse.com/docs/integrations/superagent) | Open Source AI Assistant Framework & API for prototyping and deployment of agents. |
|
||||
| Name | Description |
|
||||
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Instructor](/docs/integrations/instructor) | Library to get structured LLM outputs (JSON, Pydantic) |
|
||||
| [Mirascope](/docs/integrations/mirascope) | Python toolkit for building LLM applications. |
|
||||
| [AI SDK by Vercel](/docs/sdk/typescript/example-vercel-ai) | Typescript SDK that makes streaming LLM outputs super easy. |
|
||||
| [Flowise](/docs/integrations/flowise) | JS/TS no-code builder for customized LLM flows. |
|
||||
| [Langflow](/docs/integrations/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
|
||||
| [Superagent](/docs/integrations/superagent) | Open Source AI Assistant Framework & API for prototyping and deployment of agents. |
|
||||
|
||||
## Questions and feedback
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ Join us in scaling Langfuse in Berlin, Germany. We are an open source company, w
|
||||
|
||||
_Open Roles_
|
||||
|
||||
- Backend Engineer, 70-110k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mnrdwla-backend-engineer
|
||||
- Product Engineer, 70-110k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
|
||||
- Developer Advocate, 60-100k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
|
||||
- Backend Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mnrdwla-backend-engineer
|
||||
- Product Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
|
||||
- Developer Advocate, 60-110k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
|
||||
|
||||
_More Info_
|
||||
|
||||
|
||||
+31
-2
@@ -2,10 +2,11 @@ import { type Provider } from "next-auth/providers/index";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
import GitHubProvider from "next-auth/providers/github";
|
||||
import OktaProvider from "next-auth/providers/okta";
|
||||
import CognitoProvider from "next-auth/providers/cognito";
|
||||
import Auth0Provider from "next-auth/providers/auth0";
|
||||
import AzureADProvider from "next-auth/providers/azure-ad";
|
||||
import { isEeAvailable } from "..";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { type SsoConfig, prisma } from "@langfuse/shared/src/db";
|
||||
import { encrypt, decrypt } from "@langfuse/shared/encryption";
|
||||
import { SsoProviderSchema } from "./types";
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
@@ -25,14 +26,36 @@ let cachedSsoConfigs: {
|
||||
async function getSsoConfigs(): Promise<SsoProviderSchema[]> {
|
||||
if (!isEeAvailable) return [];
|
||||
const CACHE_TTL = 60 * 1000; // 1 minute
|
||||
const DB_MAX_WAIT = 2000; // 2 seconds
|
||||
const DB_TIMEOUT = 3000; // 3 seconds
|
||||
|
||||
// Set/refresh the cache if it's empty or expired
|
||||
if (
|
||||
cachedSsoConfigs === null ||
|
||||
Date.now() - cachedSsoConfigs.timestamp > CACHE_TTL
|
||||
) {
|
||||
// findMany with custom timeout via $transaction
|
||||
let dbConfigs: SsoConfig[] = [];
|
||||
try {
|
||||
dbConfigs = await prisma.$transaction(
|
||||
async (prisma) => prisma.ssoConfig.findMany(),
|
||||
{
|
||||
maxWait: DB_MAX_WAIT,
|
||||
timeout: DB_TIMEOUT,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
// cache empty array to prevent repeated DB calls on error
|
||||
cachedSsoConfigs = {
|
||||
data: [],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// caught and logged in the caller
|
||||
throw e;
|
||||
}
|
||||
|
||||
// transform into zod object
|
||||
const dbConfigs = await prisma.ssoConfig.findMany();
|
||||
const parsedSsoConfigs = dbConfigs
|
||||
.map((v) => {
|
||||
try {
|
||||
@@ -144,6 +167,12 @@ const dbToNextAuthProvider = (provider: SsoProviderSchema): Provider | null => {
|
||||
...provider.authConfig,
|
||||
clientSecret: decrypt(provider.authConfig.clientSecret),
|
||||
});
|
||||
else if (provider.authProvider === "cognito")
|
||||
return CognitoProvider({
|
||||
id: getAuthProviderIdForSsoConfig(provider), // use the domain as the provider id as we use domain-specific credentials
|
||||
...provider.authConfig,
|
||||
clientSecret: decrypt(provider.authConfig.clientSecret),
|
||||
});
|
||||
else {
|
||||
// Type check to ensure we handle all providers
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
@@ -64,11 +64,24 @@ export const AzureAdProviderSchema = base.extend({
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export const CognitoProviderSchema = base.extend({
|
||||
authProvider: z.literal("cognito"),
|
||||
authConfig: z
|
||||
.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
issuer: z.string(),
|
||||
allowDangerousEmailAccountLinking: z.boolean().optional().default(false),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export type GoogleProviderSchema = z.infer<typeof GoogleProviderSchema>;
|
||||
export type GithubProviderSchema = z.infer<typeof GithubProviderSchema>;
|
||||
export type Auth0ProviderSchema = z.infer<typeof Auth0ProviderSchema>;
|
||||
export type OktaProviderSchema = z.infer<typeof OktaProviderSchema>;
|
||||
export type AzureAdProviderSchema = z.infer<typeof AzureAdProviderSchema>;
|
||||
export type CognitoProviderSchema = z.infer<typeof CognitoProviderSchema>;
|
||||
|
||||
export const SsoProviderSchema = z.discriminatedUnion("authProvider", [
|
||||
GoogleProviderSchema,
|
||||
@@ -76,6 +89,7 @@ export const SsoProviderSchema = z.discriminatedUnion("authProvider", [
|
||||
Auth0ProviderSchema,
|
||||
OktaProviderSchema,
|
||||
AzureAdProviderSchema,
|
||||
CognitoProviderSchema,
|
||||
]);
|
||||
|
||||
export type SsoProviderSchema = z.infer<typeof SsoProviderSchema>;
|
||||
|
||||
@@ -209,8 +209,8 @@ types:
|
||||
- ARCHIVED
|
||||
ScoreSource:
|
||||
enum:
|
||||
- ANNOTATION
|
||||
- API
|
||||
- REVIEW
|
||||
- EVAL
|
||||
|
||||
errors:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.45.0",
|
||||
"version": "2.47.2",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -25,11 +25,16 @@ export const ObservationLevel = {
|
||||
} as const;
|
||||
export type ObservationLevel = (typeof ObservationLevel)[keyof typeof ObservationLevel];
|
||||
export const ScoreSource = {
|
||||
ANNOTATION: "ANNOTATION",
|
||||
API: "API",
|
||||
REVIEW: "REVIEW",
|
||||
EVAL: "EVAL"
|
||||
} as const;
|
||||
export type ScoreSource = (typeof ScoreSource)[keyof typeof ScoreSource];
|
||||
export const ScoreDataType = {
|
||||
CATEGORICAL: "CATEGORICAL",
|
||||
NUMERIC: "NUMERIC"
|
||||
} as const;
|
||||
export type ScoreDataType = (typeof ScoreDataType)[keyof typeof ScoreDataType];
|
||||
export const PricingUnit = {
|
||||
PER_1000_TOKENS: "PER_1000_TOKENS",
|
||||
PER_1000_CHARS: "PER_1000_CHARS"
|
||||
@@ -246,6 +251,7 @@ export type Observation = {
|
||||
status_message: string | null;
|
||||
version: string | null;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
model: string | null;
|
||||
internal_model: string | null;
|
||||
modelParameters: unknown | null;
|
||||
@@ -351,6 +357,24 @@ export type Score = {
|
||||
comment: string | null;
|
||||
trace_id: string;
|
||||
observation_id: string | null;
|
||||
config_id: string | null;
|
||||
string_value: string | null;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
data_type: Generated<ScoreDataType>;
|
||||
};
|
||||
export type ScoreConfig = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
project_id: string;
|
||||
name: string;
|
||||
data_type: ScoreDataType;
|
||||
is_archived: Generated<boolean>;
|
||||
min_value: number | null;
|
||||
max_value: number | null;
|
||||
categories: unknown | null;
|
||||
description: string | null;
|
||||
};
|
||||
export type Session = {
|
||||
id: string;
|
||||
@@ -450,6 +474,7 @@ export type DB = {
|
||||
project_memberships: ProjectMembership;
|
||||
projects: Project;
|
||||
prompts: Prompt;
|
||||
score_configs: ScoreConfig;
|
||||
scores: Score;
|
||||
Session: Session;
|
||||
sso_configs: SsoConfig;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ScoreDataType" AS ENUM ('CATEGORICAL', 'NUMERIC');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "scores" ADD COLUMN "config_id" TEXT,
|
||||
ADD COLUMN "data_type" "ScoreDataType" NOT NULL DEFAULT 'NUMERIC',
|
||||
ADD COLUMN "string_value" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "score_configs" (
|
||||
"id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"data_type" "ScoreDataType" NOT NULL,
|
||||
"is_archived" BOOLEAN NOT NULL DEFAULT false,
|
||||
"min_value" DOUBLE PRECISION,
|
||||
"max_value" DOUBLE PRECISION,
|
||||
"categories" JSONB,
|
||||
"description" TEXT,
|
||||
|
||||
CONSTRAINT "score_configs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "score_configs_data_type_idx" ON "score_configs"("data_type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "score_configs_is_archived_idx" ON "score_configs"("is_archived");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "score_configs_project_id_idx" ON "score_configs"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "score_configs_categories_idx" ON "score_configs"("categories");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "score_configs_id_project_id_key" ON "score_configs"("id", "project_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "score_configs" ADD CONSTRAINT "score_configs_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "scores_config_id_idx" ON "scores"("config_id");
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scores" ADD CONSTRAINT "scores_config_id_fkey" FOREIGN KEY ("config_id") REFERENCES "score_configs"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ScoreSource" ADD VALUE 'ANNOTATION';
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
-- Backfill the scores source for 'REVIEW' to be 'ANNOTATION'
|
||||
UPDATE "scores"
|
||||
SET "source" = 'ANNOTATION'::"ScoreSource"
|
||||
WHERE "source" = 'REVIEW'::"ScoreSource";
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- The values [REVIEW] on the enum `ScoreSource` will be removed. If these variants are still used in the database, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterEnum
|
||||
BEGIN;
|
||||
CREATE TYPE "ScoreSource_new" AS ENUM ('ANNOTATION', 'API', 'EVAL');
|
||||
ALTER TABLE "scores" ALTER COLUMN "source" TYPE "ScoreSource_new" USING ("source"::text::"ScoreSource_new");
|
||||
ALTER TYPE "ScoreSource" RENAME TO "ScoreSource_old";
|
||||
ALTER TYPE "ScoreSource_new" RENAME TO "ScoreSource";
|
||||
DROP TYPE "ScoreSource_old";
|
||||
COMMIT;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "job_executions_job_configuration_id_idx" ON "job_executions"("job_configuration_id");
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "job_executions_job_output_score_id_idx" ON "job_executions"("job_output_score_id");
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "job_executions_job_input_trace_id_idx" ON "job_executions"("job_input_trace_id");
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "job_executions_created_at_idx" ON "job_executions"("created_at");
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "observations" ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "scores" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "audit_logs_updated_at_idx" ON "audit_logs"("updated_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "dataset_items_created_at_idx" ON "dataset_items"("created_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "dataset_items_updated_at_idx" ON "dataset_items"("updated_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "dataset_run_items_created_at_idx" ON "dataset_run_items"("created_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "dataset_run_items_updated_at_idx" ON "dataset_run_items"("updated_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "dataset_runs_created_at_idx" ON "dataset_runs"("created_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "dataset_runs_updated_at_idx" ON "dataset_runs"("updated_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "datasets_created_at_idx" ON "datasets"("created_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "datasets_updated_at_idx" ON "datasets"("updated_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "job_executions_updated_at_idx" ON "job_executions"("updated_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "observations_updated_at_idx" ON "observations"("updated_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "prompts_created_at_idx" ON "prompts"("created_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "prompts_updated_at_idx" ON "prompts"("updated_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "score_configs_created_at_idx" ON "score_configs"("created_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "score_configs_updated_at_idx" ON "score_configs"("updated_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "scores_created_at_idx" ON "scores"("created_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "scores_updated_at_idx" ON "scores"("updated_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "trace_sessions_updated_at_idx" ON "trace_sessions"("updated_at");
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- Google Vertex uses @ to separate model name and version
|
||||
|
||||
UPDATE "public"."models" SET "match_pattern" = '(?i)^(claude-3-haiku(-|@)?20240307)$' WHERE "id" = 'cltr0w45b000008k1407o9qv1';
|
||||
|
||||
UPDATE "public"."models" SET "match_pattern" = '(?i)^(claude-3-opus(-|@)?20240229)$' WHERE "id" = 'cltgy0iuw000008le3vod1hhy';
|
||||
|
||||
UPDATE "public"."models" SET "match_pattern" = '(?i)^(claude-3-sonnet(-|@)?20240229)$' WHERE "id" = 'cltgy0pp6000108le56se7bl3';
|
||||
@@ -114,6 +114,7 @@ model Project {
|
||||
LlmApiKeys LlmApiKeys[]
|
||||
PosthogIntegration PosthogIntegration[]
|
||||
Score Score[]
|
||||
scoreConfig ScoreConfig[]
|
||||
|
||||
@@map("projects")
|
||||
}
|
||||
@@ -205,6 +206,7 @@ model TraceSession {
|
||||
@@id([id, projectId])
|
||||
@@index([projectId])
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@map("trace_sessions")
|
||||
}
|
||||
|
||||
@@ -292,6 +294,7 @@ model Observation {
|
||||
statusMessage String? @map("status_message")
|
||||
version String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
// GENERATION ONLY
|
||||
model String?
|
||||
@@ -323,6 +326,7 @@ model Observation {
|
||||
@@index([type])
|
||||
@@index(startTime)
|
||||
@@index(createdAt)
|
||||
@@index(updatedAt)
|
||||
@@index(projectId)
|
||||
@@index(parentObservationId)
|
||||
@@index(model)
|
||||
@@ -403,25 +407,64 @@ model Score {
|
||||
comment String?
|
||||
traceId String @map("trace_id")
|
||||
observationId String? @map("observation_id")
|
||||
configId String? @map("config_id")
|
||||
stringValue String? @map("string_value")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
dataType ScoreDataType @default(NUMERIC) @map("data_type")
|
||||
JobExecution JobExecution[]
|
||||
scoreConfig ScoreConfig? @relation(fields: [configId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([id, projectId]) // used for upserts via prisma
|
||||
@@index(timestamp)
|
||||
@@index([value])
|
||||
@@index([projectId])
|
||||
@@index([authorUserId])
|
||||
@@index([configId])
|
||||
@@index([traceId], type: Hash)
|
||||
@@index([observationId], type: Hash)
|
||||
@@index([source])
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@map("scores")
|
||||
}
|
||||
|
||||
enum ScoreSource {
|
||||
ANNOTATION
|
||||
API
|
||||
REVIEW
|
||||
EVAL
|
||||
}
|
||||
|
||||
model ScoreConfig {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
dataType ScoreDataType @map("data_type")
|
||||
isArchived Boolean @default(false) @map("is_archived")
|
||||
minValue Float? @map("min_value")
|
||||
maxValue Float? @map("max_value")
|
||||
categories Json? @map("categories")
|
||||
description String?
|
||||
score Score[]
|
||||
|
||||
@@unique([id, projectId]) // used for upserts via prisma
|
||||
@@index([dataType])
|
||||
@@index([isArchived])
|
||||
@@index([projectId])
|
||||
@@index([categories])
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@map("score_configs")
|
||||
}
|
||||
|
||||
enum ScoreDataType {
|
||||
CATEGORICAL
|
||||
NUMERIC
|
||||
}
|
||||
|
||||
enum PricingUnit {
|
||||
PER_1000_TOKENS
|
||||
PER_1000_CHARS
|
||||
@@ -468,6 +511,8 @@ model Dataset {
|
||||
|
||||
@@unique([projectId, name])
|
||||
@@index([projectId], type: Hash)
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@map("datasets")
|
||||
}
|
||||
|
||||
@@ -489,6 +534,8 @@ model DatasetItem {
|
||||
|
||||
@@index([sourceObservationId], type: Hash)
|
||||
@@index([datasetId], type: Hash)
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@map("dataset_items")
|
||||
}
|
||||
|
||||
@@ -510,6 +557,8 @@ model DatasetRuns {
|
||||
|
||||
@@unique([datasetId, name])
|
||||
@@index([datasetId], type: Hash)
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@map("dataset_runs")
|
||||
}
|
||||
|
||||
@@ -530,6 +579,8 @@ model DatasetRunItems {
|
||||
@@index([datasetItemId], type: Hash)
|
||||
@@index([observationId], type: Hash)
|
||||
@@index([traceId])
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@map("dataset_run_items")
|
||||
}
|
||||
|
||||
@@ -572,6 +623,8 @@ model Prompt {
|
||||
@@index([projectId, name, version])
|
||||
@@index([projectId, id])
|
||||
@@index([projectId])
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@index([tags(ops: ArrayOps)], type: Gin)
|
||||
@@map("prompts")
|
||||
}
|
||||
@@ -619,6 +672,7 @@ model AuditLog {
|
||||
|
||||
@@index([projectId])
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@map("audit_logs")
|
||||
}
|
||||
|
||||
@@ -712,6 +766,11 @@ model JobExecution {
|
||||
@@index([projectId, status])
|
||||
@@index([projectId, id])
|
||||
@@index([projectId])
|
||||
@@index([jobConfigurationId])
|
||||
@@index([jobOutputScoreId])
|
||||
@@index([jobInputTraceId])
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@map("job_executions")
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { chunk } from "lodash";
|
||||
import { v4 } from "uuid";
|
||||
import { ModelUsageUnit } from "../src";
|
||||
import { getDisplaySecretKey, hashSecretKey } from "../src/server/auth";
|
||||
import { encrypt } from "../src/encryption";
|
||||
|
||||
const LOAD_TRACE_VOLUME = 10_000;
|
||||
|
||||
@@ -165,6 +166,24 @@ async function main() {
|
||||
|
||||
await uploadObjects(traces, observations, scores, sessions, events);
|
||||
|
||||
// If openai key is in environment, add it to the projects LLM API keys
|
||||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
if (OPENAI_API_KEY) {
|
||||
await prisma.llmApiKeys.create({
|
||||
data: {
|
||||
projectId: project1.id,
|
||||
secretKey: encrypt(OPENAI_API_KEY),
|
||||
displaySecretKey: getDisplaySecretKey(OPENAI_API_KEY),
|
||||
provider: "openai",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.warn(
|
||||
"No OPENAI_API_KEY found in environment. Skipping seeding LLM API key."
|
||||
);
|
||||
}
|
||||
|
||||
// add eval objects
|
||||
const evalTemplate = await prisma.evalTemplate.upsert({
|
||||
where: {
|
||||
@@ -500,7 +519,7 @@ function createObjects(
|
||||
name: "manual-score",
|
||||
value: Math.floor(Math.random() * 3) - 1,
|
||||
timestamp: traceTs,
|
||||
source: ScoreSource.REVIEW,
|
||||
source: ScoreSource.ANNOTATION,
|
||||
projectId,
|
||||
authorUserId: `user-${i}`,
|
||||
},
|
||||
|
||||
Generated
+20
-9
@@ -222,6 +222,9 @@ importers:
|
||||
'@codemirror/lang-json':
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
'@codemirror/lint':
|
||||
specifier: ^6.8.0
|
||||
version: 6.8.0
|
||||
'@headlessui/react':
|
||||
specifier: 1.7.18
|
||||
version: 1.7.18(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -380,7 +383,7 @@ importers:
|
||||
version: 4.22.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)
|
||||
'@uiw/react-codemirror':
|
||||
specifier: ^4.21.25
|
||||
version: 4.21.25(@babel/runtime@7.24.5)(@codemirror/autocomplete@6.16.0)(@codemirror/language@6.10.1)(@codemirror/lint@6.7.1)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.3)(codemirror@6.0.1)(react-dom@18.2.0)(react@18.2.0)
|
||||
version: 4.21.25(@babel/runtime@7.24.5)(@codemirror/autocomplete@6.16.0)(@codemirror/language@6.10.1)(@codemirror/lint@6.8.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.3)(codemirror@6.0.1)(react-dom@18.2.0)(react@18.2.0)
|
||||
ai:
|
||||
specifier: ^3.0.23
|
||||
version: 3.0.23(react@18.2.0)(solid-js@1.8.17)(svelte@4.2.17)(vue@3.4.27)(zod@3.22.4)
|
||||
@@ -466,8 +469,8 @@ importers:
|
||||
specifier: ^7.51.5
|
||||
version: 7.51.5(react@18.2.0)
|
||||
react-icons:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1(react@18.2.0)
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1(react@18.2.0)
|
||||
react-responsive:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0(react@18.2.0)
|
||||
@@ -2084,6 +2087,14 @@ packages:
|
||||
crelt: 1.0.6
|
||||
dev: false
|
||||
|
||||
/@codemirror/lint@6.8.0:
|
||||
resolution: {integrity: sha512-lsFofvaw0lnPRJlQylNsC4IRt/1lI4OD/yYslrSGVndOJfStc58v+8p9dgGiD90ktOfL7OhBWns1ZETYgz0EJA==}
|
||||
dependencies:
|
||||
'@codemirror/state': 6.4.1
|
||||
'@codemirror/view': 6.26.3
|
||||
crelt: 1.0.6
|
||||
dev: false
|
||||
|
||||
/@codemirror/search@6.5.6:
|
||||
resolution: {integrity: sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==}
|
||||
dependencies:
|
||||
@@ -7111,7 +7122,7 @@ packages:
|
||||
eslint-visitor-keys: 3.4.3
|
||||
dev: true
|
||||
|
||||
/@uiw/codemirror-extensions-basic-setup@4.21.25(@codemirror/autocomplete@6.16.0)(@codemirror/commands@6.3.3)(@codemirror/language@6.10.1)(@codemirror/lint@6.7.1)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3):
|
||||
/@uiw/codemirror-extensions-basic-setup@4.21.25(@codemirror/autocomplete@6.16.0)(@codemirror/commands@6.3.3)(@codemirror/language@6.10.1)(@codemirror/lint@6.8.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3):
|
||||
resolution: {integrity: sha512-eeUKlmEE8aSoSgelS8OR2elcPGntpRo669XinAqPCLa0eKorT2B0d3ts+AE+njAeGk744tiyAEbHb2n+6OQmJw==}
|
||||
peerDependencies:
|
||||
'@codemirror/autocomplete': '>=6.0.0'
|
||||
@@ -7125,7 +7136,7 @@ packages:
|
||||
'@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
|
||||
'@codemirror/commands': 6.3.3
|
||||
'@codemirror/language': 6.10.1
|
||||
'@codemirror/lint': 6.7.1
|
||||
'@codemirror/lint': 6.8.0
|
||||
'@codemirror/search': 6.5.6
|
||||
'@codemirror/state': 6.4.1
|
||||
'@codemirror/view': 6.26.3
|
||||
@@ -7175,7 +7186,7 @@ packages:
|
||||
'@codemirror/view': 6.26.3
|
||||
dev: false
|
||||
|
||||
/@uiw/react-codemirror@4.21.25(@babel/runtime@7.24.5)(@codemirror/autocomplete@6.16.0)(@codemirror/language@6.10.1)(@codemirror/lint@6.7.1)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.3)(codemirror@6.0.1)(react-dom@18.2.0)(react@18.2.0):
|
||||
/@uiw/react-codemirror@4.21.25(@babel/runtime@7.24.5)(@codemirror/autocomplete@6.16.0)(@codemirror/language@6.10.1)(@codemirror/lint@6.8.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.3)(codemirror@6.0.1)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-mBrCoiffQ+hbTqV1JoixFEcH7BHXkS3PjTyNH7dE8Gzf3GSBRazhtSM5HrAFIiQ5FIRGFs8Gznc4UAdhtevMmw==}
|
||||
peerDependencies:
|
||||
'@babel/runtime': '>=7.11.0'
|
||||
@@ -7191,7 +7202,7 @@ packages:
|
||||
'@codemirror/state': 6.4.1
|
||||
'@codemirror/theme-one-dark': 6.1.2
|
||||
'@codemirror/view': 6.26.3
|
||||
'@uiw/codemirror-extensions-basic-setup': 4.21.25(@codemirror/autocomplete@6.16.0)(@codemirror/commands@6.3.3)(@codemirror/language@6.10.1)(@codemirror/lint@6.7.1)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)
|
||||
'@uiw/codemirror-extensions-basic-setup': 4.21.25(@codemirror/autocomplete@6.16.0)(@codemirror/commands@6.3.3)(@codemirror/language@6.10.1)(@codemirror/lint@6.8.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)
|
||||
codemirror: 6.0.1(@lezer/common@1.2.1)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
@@ -15018,8 +15029,8 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/react-icons@5.0.1(react@18.2.0):
|
||||
resolution: {integrity: sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==}
|
||||
/react-icons@5.2.1(react@18.2.0):
|
||||
resolution: {integrity: sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
dependencies:
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.45.0",
|
||||
"version": "2.47.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -25,6 +25,7 @@
|
||||
"@aws-sdk/lib-storage": "^3.568.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.554.0",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lint": "^6.8.0",
|
||||
"@headlessui/react": "1.7.18",
|
||||
"@headlessui/tailwindcss": "0.2.0",
|
||||
"@heroicons/react": "^2.1.3",
|
||||
@@ -106,7 +107,7 @@
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.51.5",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react18-json-view": "^0.2.8-canary.6",
|
||||
"sonner": "^1.4.41",
|
||||
|
||||
@@ -1825,8 +1825,8 @@ components:
|
||||
title: ScoreSource
|
||||
type: string
|
||||
enum:
|
||||
- ANNOTATION
|
||||
- API
|
||||
- REVIEW
|
||||
- EVAL
|
||||
CreateDatasetItemRequest:
|
||||
title: CreateDatasetItemRequest
|
||||
|
||||
@@ -489,16 +489,16 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
totalPages: 1,
|
||||
});
|
||||
expect(getScore.body.data).toMatchObject([
|
||||
{
|
||||
id: scoreId_1,
|
||||
name: scoreName,
|
||||
value: 10.5,
|
||||
},
|
||||
{
|
||||
id: scoreId_2,
|
||||
name: scoreName,
|
||||
value: 50.5,
|
||||
},
|
||||
{
|
||||
id: scoreId_1,
|
||||
name: scoreName,
|
||||
value: 10.5,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("test operator >=", async () => {
|
||||
@@ -514,16 +514,16 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
totalPages: 1,
|
||||
});
|
||||
expect(getScore.body.data).toMatchObject([
|
||||
{
|
||||
id: scoreId_2,
|
||||
name: scoreName,
|
||||
value: 50.5,
|
||||
},
|
||||
{
|
||||
id: scoreId_3,
|
||||
name: scoreName,
|
||||
value: 100.8,
|
||||
},
|
||||
{
|
||||
id: scoreId_2,
|
||||
name: scoreName,
|
||||
value: 50.5,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("test operator !=", async () => {
|
||||
@@ -539,16 +539,16 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
totalPages: 1,
|
||||
});
|
||||
expect(getScore.body.data).toMatchObject([
|
||||
{
|
||||
id: scoreId_1,
|
||||
name: scoreName,
|
||||
value: 10.5,
|
||||
},
|
||||
{
|
||||
id: scoreId_3,
|
||||
name: scoreName,
|
||||
value: 100.8,
|
||||
},
|
||||
{
|
||||
id: scoreId_1,
|
||||
name: scoreName,
|
||||
value: 10.5,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("test operator =", async () => {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import chatCompletionHandler from "@/src/ee/features/playground/server/chatCompletionHandler";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const maxDuration = 60;
|
||||
|
||||
export const POST = chatCompletionHandler;
|
||||
@@ -0,0 +1,77 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { Label } from "@/src/components/ui/label";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { supportedModels, type UIModelParams } from "@langfuse/shared";
|
||||
import { ArrowTopRightIcon } from "@radix-ui/react-icons";
|
||||
|
||||
export const LLMApiKeyComponent = (p: {
|
||||
projectId: string;
|
||||
modelParams: UIModelParams;
|
||||
}) => {
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: p.projectId,
|
||||
scope: "llmApiKeys:read",
|
||||
});
|
||||
|
||||
if (!hasAccess) {
|
||||
return (
|
||||
<div>
|
||||
<Label className="text-xs font-semibold">API key</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
LLM API Key only visible to Owner and Admin roles.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const apiKeys = api.llmApiKey.all.useQuery({
|
||||
projectId: p.projectId,
|
||||
});
|
||||
|
||||
if (apiKeys.isLoading) {
|
||||
return (
|
||||
<div>
|
||||
<Label className="text-xs font-semibold">API key</Label>
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const model = p.modelParams.model.value;
|
||||
const modelProvider = Object.entries(supportedModels).find((providerData) =>
|
||||
(providerData[1] as any as string[]).includes(model),
|
||||
)?.[0];
|
||||
|
||||
const apiKey = apiKeys.data?.data.find((k) => k.provider === modelProvider);
|
||||
|
||||
return (
|
||||
<div className="space-y-2 text-xs">
|
||||
<Label className="text-xs font-semibold">API key</Label>
|
||||
<div>
|
||||
{apiKey ? (
|
||||
<span className="mr-2 rounded-sm bg-input p-1 text-xs">
|
||||
{apiKey.displaySecretKey}
|
||||
</span>
|
||||
) : undefined}
|
||||
</div>
|
||||
{/* Custom form message to include a link to the already existing prompt */}
|
||||
{!apiKey ? (
|
||||
<div className="flex flex-col font-medium text-destructive">
|
||||
{`No LLM API key found for provider ${modelProvider}.`}
|
||||
|
||||
<Link
|
||||
href={`/project/${p.projectId}/settings`}
|
||||
className="flex flex-row"
|
||||
>
|
||||
Create a new LLM API key here. <ArrowTopRightIcon />
|
||||
</Link>
|
||||
</div>
|
||||
) : undefined}
|
||||
<p className="text-muted-foreground">
|
||||
The LLM API key is used for each execution and will incur costs.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+9
-1
@@ -8,6 +8,7 @@ import {
|
||||
} from "@/src/components/ui/select";
|
||||
import { Slider } from "@/src/components/ui/slider";
|
||||
import { Switch } from "@/src/components/ui/switch";
|
||||
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import {
|
||||
ModelProvider,
|
||||
@@ -15,6 +16,8 @@ import {
|
||||
type UIModelParams,
|
||||
} from "@langfuse/shared";
|
||||
|
||||
import { LLMApiKeyComponent } from "./LLMApiKeyComponent";
|
||||
|
||||
export type ModelParamsContext = {
|
||||
modelParams: UIModelParams;
|
||||
availableModels?: UIModelParams[];
|
||||
@@ -33,10 +36,14 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
|
||||
setModelParamEnabled,
|
||||
formDisabled = false,
|
||||
}) => {
|
||||
const projectId = useProjectIdFromURL();
|
||||
|
||||
if (!projectId) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<p className="font-semibold">Model</p>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<ModelParamsSelect
|
||||
title="Provider"
|
||||
modelParamsKey="provider"
|
||||
@@ -104,6 +111,7 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
|
||||
tooltip="An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both."
|
||||
updateModelParam={updateModelParamValue}
|
||||
/>
|
||||
<LLMApiKeyComponent {...{ projectId, modelParams }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -8,14 +8,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/src/components/ui/card";
|
||||
import { chatAvailable, openChat } from "@/src/features/support-chat/chat";
|
||||
import {
|
||||
Book,
|
||||
Calendar,
|
||||
Github,
|
||||
Mail,
|
||||
MessageSquare,
|
||||
Slack,
|
||||
} from "lucide-react";
|
||||
import { Book, Github, Mail, MessageSquare, Slack } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { SiDiscord } from "react-icons/si";
|
||||
|
||||
@@ -30,7 +23,7 @@ const supportChannels = [
|
||||
},
|
||||
{
|
||||
icon: Github,
|
||||
title: "GitHub",
|
||||
title: "GitHub Issues",
|
||||
description:
|
||||
"Create an issue on Github to report bugs or request new features.",
|
||||
href: "https://github.com/langfuse/langfuse/issues/new/choose",
|
||||
@@ -38,12 +31,11 @@ const supportChannels = [
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
icon: SiDiscord,
|
||||
title: "Discord",
|
||||
description:
|
||||
"Get support from community and maintainers. Follow announcements to stay up to date with new features.",
|
||||
href: "https://langfuse.com/discord",
|
||||
buttonText: "Join Discord",
|
||||
icon: Github,
|
||||
title: "GitHub Support",
|
||||
description: "Create a support ticket via GitHub discussions.",
|
||||
href: "https://github.com/orgs/langfuse/discussions/categories/support",
|
||||
buttonText: "Submit question",
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
@@ -57,17 +49,18 @@ const supportChannels = [
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: "Chat",
|
||||
description: "Get quick support directly from the team.",
|
||||
description: "Get support directly from the team.",
|
||||
onClick: () => openChat(),
|
||||
available: chatAvailable,
|
||||
buttonText: "Launch Chat",
|
||||
},
|
||||
{
|
||||
icon: Calendar,
|
||||
title: "Schedule call",
|
||||
description: "Schedule a call with one of the founders.",
|
||||
href: "https://cal.com/marc-kl/office-hours",
|
||||
buttonText: "Schedule Call",
|
||||
icon: SiDiscord,
|
||||
title: "Discord",
|
||||
description:
|
||||
"Get support from community. Follow announcements to stay up to date with new features.",
|
||||
href: "https://langfuse.com/discord",
|
||||
buttonText: "Join Discord",
|
||||
},
|
||||
{
|
||||
icon: Slack,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||
import { githubLight } from "@uiw/codemirror-theme-github";
|
||||
import { tokyoNight } from "@uiw/codemirror-theme-tokyo-night";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { json, jsonParseLinter } from "@codemirror/lang-json";
|
||||
import { linter } from "@codemirror/lint";
|
||||
import { useTheme } from "next-themes";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { useState } from "react";
|
||||
|
||||
// todo: add json linting
|
||||
|
||||
@@ -20,8 +22,13 @@ export function JsonEditor({
|
||||
lineWrapping?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const codeMirrorTheme = theme === "dark" ? tokyoNight : githubLight;
|
||||
const { resolvedTheme } = useTheme();
|
||||
const codeMirrorTheme = resolvedTheme === "dark" ? tokyoNight : githubLight;
|
||||
|
||||
// used to disable linter when field is empty
|
||||
const [linterEnabled, setLinterEnabled] = useState<boolean>(
|
||||
!!defaultValue && defaultValue !== "",
|
||||
);
|
||||
return (
|
||||
<CodeMirror
|
||||
value={defaultValue}
|
||||
@@ -30,9 +37,16 @@ export function JsonEditor({
|
||||
foldGutter: true,
|
||||
}}
|
||||
lang={"json"}
|
||||
extensions={[json(), ...(lineWrapping ? [EditorView.lineWrapping] : [])]}
|
||||
extensions={[
|
||||
json(),
|
||||
...(linterEnabled ? [linter(jsonParseLinter())] : []),
|
||||
...(lineWrapping ? [EditorView.lineWrapping] : []),
|
||||
]}
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
onChange={(c) => {
|
||||
if (onChange) onChange(c);
|
||||
setLinterEnabled(c !== "");
|
||||
}}
|
||||
className={cn("overflow-hidden rounded-md border", className)}
|
||||
editable={editable}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
HoverCardTrigger,
|
||||
} from "@/src/components/ui/hover-card";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { HelpCircle, Info } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -40,7 +41,10 @@ export default function DocPopup({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<HoverCardTrigger className="mx-1 cursor-pointer" asChild>
|
||||
<HoverCardTrigger
|
||||
className={cn("mx-1", href ? "cursor-pointer" : "cursor-default")}
|
||||
asChild
|
||||
>
|
||||
{href ? (
|
||||
<Link
|
||||
href={href}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { cn } from "@/src/utils/tailwind";
|
||||
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
|
||||
import { useState } from "react";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { trpcErrorToast } from "@/src/utils/trpcErrorToast";
|
||||
|
||||
export function StarToggle({
|
||||
value,
|
||||
@@ -76,7 +77,7 @@ export function StarTraceToggle({
|
||||
onError: (err, _newTodo, context) => {
|
||||
setIsLoading(false);
|
||||
// Rollback to the previous value if mutation fails
|
||||
console.log("error", err);
|
||||
trpcErrorToast(err);
|
||||
utils.traces.all.setData(tracesFilter, context?.prev);
|
||||
},
|
||||
onSettled: () => {
|
||||
@@ -157,7 +158,7 @@ export function StarTraceDetailsToggle({
|
||||
},
|
||||
onError: (err, _newTodo, context) => {
|
||||
setIsLoading(false);
|
||||
console.log("error", err);
|
||||
trpcErrorToast(err);
|
||||
// Rollback to the previous value if mutation fails
|
||||
utils.traces.byId.setData({ traceId }, context?.prevData);
|
||||
},
|
||||
|
||||
@@ -21,11 +21,7 @@ import {
|
||||
withDefault,
|
||||
} from "use-query-params";
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import {
|
||||
formatIntervalSeconds,
|
||||
intervalInSeconds,
|
||||
utcDateOffsetByDays,
|
||||
} from "@/src/utils/dates";
|
||||
import { formatIntervalSeconds, utcDateOffsetByDays } from "@/src/utils/dates";
|
||||
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import {
|
||||
@@ -119,7 +115,7 @@ export default function GenerationsTable({
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(
|
||||
session.data?.environment.defaultTableDateTimeOffset ?? -14,
|
||||
session.data?.environment.defaultTableDateTimeOffset ?? -7,
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -12,8 +12,10 @@ import {
|
||||
scoresTableColsWithOptions,
|
||||
} from "@/src/server/api/definitions/scoresTable";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { utcDateOffsetByDays } from "@/src/utils/dates";
|
||||
import type { RouterOutput, RouterInput } from "@/src/utils/types";
|
||||
import type { FilterState } from "@langfuse/shared";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
|
||||
|
||||
export type ScoresTableRow = {
|
||||
@@ -72,11 +74,20 @@ export default function ScoresTable({
|
||||
pageIndex: withDefault(NumberParam, 0),
|
||||
pageSize: withDefault(NumberParam, 50),
|
||||
});
|
||||
|
||||
const session = useSession();
|
||||
const [rowHeight, setRowHeight] = useRowHeightLocalStorage("scores", "s");
|
||||
|
||||
const [userFilterState, setUserFilterState] = useQueryFilterState(
|
||||
[],
|
||||
[
|
||||
{
|
||||
column: "Timestamp",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(
|
||||
session.data?.environment.defaultTableDateTimeOffset ?? -7,
|
||||
),
|
||||
},
|
||||
],
|
||||
"scores",
|
||||
);
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function SessionsTable({
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(
|
||||
session.data?.environment.defaultTableDateTimeOffset ?? -14,
|
||||
session.data?.environment.defaultTableDateTimeOffset ?? -7,
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function TracesTable({
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(
|
||||
session.data?.environment.defaultTableDateTimeOffset ?? -14,
|
||||
session.data?.environment.defaultTableDateTimeOffset ?? -7,
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -547,6 +547,7 @@ export default function TracesTable({
|
||||
header: "Version",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "release",
|
||||
@@ -554,6 +555,7 @@ export default function TracesTable({
|
||||
header: "Release",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "tags",
|
||||
|
||||
@@ -277,6 +277,7 @@ export function TracePage({ traceId }: { traceId: string }) {
|
||||
availableTags={allTags}
|
||||
traceId={trace.data.id}
|
||||
projectId={trace.data.projectId}
|
||||
className="flex-wrap"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ export function JSONView(props: {
|
||||
}) {
|
||||
// some users ingest stringified json nested in json, parse it
|
||||
const parsedJson = deepParseJson(props.json);
|
||||
const { theme } = useTheme();
|
||||
const { resolvedTheme } = useTheme();
|
||||
return (
|
||||
<div className={cn("rounded-md border", props.className)}>
|
||||
{props.title ? (
|
||||
@@ -44,7 +44,7 @@ export function JSONView(props: {
|
||||
<React18JsonView
|
||||
src={parsedJson}
|
||||
theme="github"
|
||||
dark={theme === "dark"}
|
||||
dark={resolvedTheme === "dark"}
|
||||
collapseObjectsAfterLength={20}
|
||||
collapseStringsAfterLength={500}
|
||||
displaySize={"collapsed"}
|
||||
|
||||
@@ -19,6 +19,8 @@ const buttonVariants = cva(
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
errorNotification:
|
||||
"bg-destructive-foreground/90 text-destructive hover:bg-destructive-foreground/80",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
|
||||
@@ -42,7 +42,7 @@ const CommandInput = React.forwardRef<
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
className="flex items-center rounded-lg border px-3"
|
||||
className="flex items-center rounded-md border px-3"
|
||||
cmdk-input-wrapper=""
|
||||
>
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
|
||||
@@ -74,7 +74,7 @@ const TableHead = React.forwardRef<
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"sticky top-0 z-10 border-b bg-background px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
"sticky top-0 z-10 rounded-t-md border-b bg-background px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.45.0";
|
||||
export const VERSION = "v2.47.2";
|
||||
|
||||
@@ -33,8 +33,6 @@ import {
|
||||
type ModelParams,
|
||||
} from "@langfuse/shared";
|
||||
import { PromptDescription } from "@/src/features/prompts/components/prompt-description";
|
||||
import Link from "next/dist/client/link";
|
||||
import { ArrowTopRightIcon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -43,8 +41,6 @@ import {
|
||||
SelectValue,
|
||||
} from "@/src/components/ui/select";
|
||||
import { TEMPLATES } from "@/src/ee/features/evals/components/templates";
|
||||
import { Label } from "@/src/components/ui/label";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { getFinalModelParams } from "@/src/ee/utils/getFinalModelParams";
|
||||
|
||||
@@ -439,10 +435,6 @@ export const InnerEvalTemplateForm = (props: {
|
||||
availableModels={[...evalLLMModels]}
|
||||
formDisabled={!props.isEditing}
|
||||
/>
|
||||
<LLMApiKeyComponent
|
||||
projectId={props.projectId}
|
||||
modelParams={modelParams}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -465,78 +457,6 @@ export const InnerEvalTemplateForm = (props: {
|
||||
);
|
||||
};
|
||||
|
||||
export const LLMApiKeyComponent = (p: {
|
||||
projectId: string;
|
||||
modelParams: UIModelParams;
|
||||
}) => {
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: p.projectId,
|
||||
scope: "llmApiKeys:read",
|
||||
});
|
||||
|
||||
if (!hasAccess) {
|
||||
return (
|
||||
<div>
|
||||
<Label>API key</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
LLM API Key only visible to Owner and Admin roles.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const apiKeys = api.llmApiKey.all.useQuery({
|
||||
projectId: p.projectId,
|
||||
});
|
||||
|
||||
if (apiKeys.isLoading) {
|
||||
return (
|
||||
<div>
|
||||
<Label>API key</Label>
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getModelProvider = (model: string) => {
|
||||
return evalLLMModels.find((m) => m.model.value === model)?.provider.value;
|
||||
};
|
||||
|
||||
const getApiKeyForModel = (model: string) => {
|
||||
const modelProvider = getModelProvider(model);
|
||||
return apiKeys.data?.data.find((k) => k.provider === modelProvider);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label>API key</Label>
|
||||
<div>
|
||||
{getApiKeyForModel(p.modelParams.model.value) ? (
|
||||
<span className="mr-2 rounded-sm bg-input p-1 text-xs">
|
||||
{getApiKeyForModel(p.modelParams.model.value)?.displaySecretKey}
|
||||
</span>
|
||||
) : undefined}
|
||||
</div>
|
||||
{/* Custom form message to include a link to the already existing prompt */}
|
||||
{!getApiKeyForModel(p.modelParams.model.value) ? (
|
||||
<div className="flex flex-col text-sm font-medium text-destructive">
|
||||
{"No LLM API key found."}
|
||||
|
||||
<Link
|
||||
href={`/project/${p.projectId}/settings`}
|
||||
className="flex flex-row"
|
||||
>
|
||||
Create a new API key here. <ArrowTopRightIcon />
|
||||
</Link>
|
||||
</div>
|
||||
) : undefined}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The API key is used for each evaluation and will incur costs.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getModelParamsWithEnabledFlag(
|
||||
evalPreFill?: EvalTemplateFormPreFill,
|
||||
): UIModelParams {
|
||||
|
||||
@@ -14,6 +14,7 @@ import useCommandEnter from "@/src/ee/features/playground/page/hooks/useCommandE
|
||||
import usePlaygroundCache from "@/src/ee/features/playground/page/hooks/usePlaygroundCache";
|
||||
import { getFinalModelParams } from "@/src/ee/utils/getFinalModelParams";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
|
||||
import { extractVariables } from "@/src/utils/string";
|
||||
import {
|
||||
ChatMessageRole,
|
||||
@@ -57,6 +58,7 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const capture = usePostHogClientCapture();
|
||||
const projectId = useProjectIdFromURL();
|
||||
const { playgroundCache, setPlaygroundCache } = usePlaygroundCache();
|
||||
const [promptVariables, setPromptVariables] = useState<PromptVariable[]>([]);
|
||||
const [output, setOutput] = useState("");
|
||||
@@ -172,6 +174,7 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
|
||||
}
|
||||
|
||||
const completionStream = getChatCompletionStream(
|
||||
projectId,
|
||||
finalMessages,
|
||||
modelParams,
|
||||
);
|
||||
@@ -263,10 +266,17 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
|
||||
};
|
||||
|
||||
async function* getChatCompletionStream(
|
||||
projectId: string | undefined,
|
||||
messages: ChatMessageWithId[],
|
||||
modelParams: UIModelParams,
|
||||
) {
|
||||
if (!projectId) {
|
||||
console.error("Project ID is not set");
|
||||
return;
|
||||
}
|
||||
|
||||
const body = JSON.stringify({
|
||||
projectId,
|
||||
messages,
|
||||
modelParams: getFinalModelParams(modelParams),
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function Playground() {
|
||||
<div className="basis-[55%] ">
|
||||
<ModelParameters {...playgroundContext} />
|
||||
</div>
|
||||
<div className="basis-[45%] overflow-auto">
|
||||
<div className="mt-4 basis-[45%] overflow-auto">
|
||||
<Variables />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { getIsCloudEnvironment } from "@/src/ee/utils/getIsCloudEnvironment";
|
||||
import { getAuthOptions } from "@/src/server/auth";
|
||||
import { isProjectMemberOrAdmin } from "@/src/server/utils/checkProjectMembershipOrAdmin";
|
||||
import { ApiError, ForbiddenError, UnauthorizedError } from "@langfuse/shared";
|
||||
|
||||
export type AuthorizeRequestResult = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export const authorizeRequestOrThrow = async (
|
||||
projectId: string,
|
||||
): Promise<AuthorizeRequestResult> => {
|
||||
if (!getIsCloudEnvironment())
|
||||
throw new ApiError("This endpoint is available in Langfuse cloud only.");
|
||||
|
||||
const authOptions = await getAuthOptions();
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) throw new UnauthorizedError("Unauthenticated");
|
||||
|
||||
if (!isProjectMemberOrAdmin(session.user, projectId))
|
||||
throw new ForbiddenError("User is not a member of this project");
|
||||
|
||||
return { userId: session.user.id };
|
||||
};
|
||||
@@ -1,75 +1,60 @@
|
||||
import { StreamingTextResponse } from "ai";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
|
||||
import { fetchLLMCompletion } from "@langfuse/shared";
|
||||
import {
|
||||
BaseError,
|
||||
ValidationError,
|
||||
fetchLLMCompletion,
|
||||
} from "@langfuse/shared";
|
||||
|
||||
import { PosthogCallbackHandler } from "./analytics/posthogCallback";
|
||||
import {
|
||||
validateChatCompletionBody,
|
||||
type ValidatedChatCompletionBody,
|
||||
} from "./validateChatCompletionBody";
|
||||
import { getCookieName } from "@/src/server/utils/cookies";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { getIsCloudEnvironment } from "@/src/ee/utils/getIsCloudEnvironment";
|
||||
import { authorizeRequestOrThrow } from "./authorizeRequest";
|
||||
import { validateChatCompletionBody } from "./validateChatCompletionBody";
|
||||
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { decrypt } from "@langfuse/shared/encryption";
|
||||
|
||||
export default async function chatCompletionHandler(req: NextRequest) {
|
||||
if (!getIsCloudEnvironment()) {
|
||||
return NextResponse.json(
|
||||
{ message: "This endpoint is available in Langfuse cloud only." },
|
||||
{ status: 501 },
|
||||
);
|
||||
}
|
||||
|
||||
const token = await getToken({
|
||||
req,
|
||||
cookieName: getCookieName("next-auth.session-token"),
|
||||
secret: env.NEXTAUTH_SECRET,
|
||||
});
|
||||
|
||||
if (!token || !token.sub)
|
||||
// sub is the user id
|
||||
return NextResponse.json({ message: "Unauthenticated" }, { status: 401 });
|
||||
|
||||
if (req.method !== "POST")
|
||||
return NextResponse.json(
|
||||
{ message: "Method not allowed" },
|
||||
{ status: 405 },
|
||||
);
|
||||
|
||||
let body: ValidatedChatCompletionBody;
|
||||
|
||||
try {
|
||||
body = validateChatCompletionBody(await req.json());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const body = validateChatCompletionBody(await req.json());
|
||||
const { userId } = await authorizeRequestOrThrow(body.projectId);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: "Invalid request body",
|
||||
error: err,
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const { messages, modelParams } = body;
|
||||
|
||||
const LLMApiKey = await prisma.llmApiKeys.findFirst({
|
||||
where: {
|
||||
projectId: body.projectId,
|
||||
provider: modelParams.provider,
|
||||
},
|
||||
});
|
||||
|
||||
if (!LLMApiKey)
|
||||
throw new ValidationError(
|
||||
`No ${modelParams.provider} API key found in project. Please add one in the project settings.`,
|
||||
);
|
||||
|
||||
const stream = await fetchLLMCompletion({
|
||||
messages,
|
||||
modelParams,
|
||||
streaming: true,
|
||||
callbacks: [new PosthogCallbackHandler("playground", body, token.sub)],
|
||||
apiKey:
|
||||
modelParams.provider === "openai"
|
||||
? env.OPENAI_API_KEY
|
||||
: env.ANTHROPIC_API_KEY,
|
||||
callbacks: [new PosthogCallbackHandler("playground", body, userId)],
|
||||
apiKey: decrypt(LLMApiKey.secretKey),
|
||||
});
|
||||
|
||||
return new StreamingTextResponse(stream);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
if (err instanceof BaseError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: err.name,
|
||||
message: err.message,
|
||||
},
|
||||
{ status: err.httpCode },
|
||||
);
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ const MessageSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
});
|
||||
export const ChatCompletionBodySchema = z.object({
|
||||
projectId: z.string(),
|
||||
messages: z.array(MessageSchema),
|
||||
modelParams: ModelParamsSchema,
|
||||
});
|
||||
|
||||
@@ -67,6 +67,10 @@ export const env = createEnv({
|
||||
AUTH_AUTH0_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_AUTH0_ISSUER: z.string().url().optional(),
|
||||
AUTH_AUTH0_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
|
||||
AUTH_COGNITO_CLIENT_ID: z.string().optional(),
|
||||
AUTH_COGNITO_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_COGNITO_ISSUER: z.string().url().optional(),
|
||||
AUTH_COGNITO_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
|
||||
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT: z.string().optional(),
|
||||
AUTH_DISABLE_USERNAME_PASSWORD: z.enum(["true", "false"]).optional(),
|
||||
AUTH_DISABLE_SIGNUP: z.enum(["true", "false"]).optional(),
|
||||
@@ -167,6 +171,10 @@ export const env = createEnv({
|
||||
AUTH_AUTH0_ISSUER: process.env.AUTH_AUTH0_ISSUER,
|
||||
AUTH_AUTH0_ALLOW_ACCOUNT_LINKING:
|
||||
process.env.AUTH_AUTH0_ALLOW_ACCOUNT_LINKING,
|
||||
AUTH_COGNITO_CLIENT_ID: process.env.AUTH_COGNITO_CLIENT_ID,
|
||||
AUTH_COGNITO_CLIENT_SECRET: process.env.AUTH_COGNITO_CLIENT_SECRET,
|
||||
AUTH_COGNITO_ISSUER: process.env.AUTH_COGNITO_ISSUER,
|
||||
AUTH_COGNITO_ALLOW_ACCOUNT_LINKING: process.env.AUTH_COGNITO_ALLOW_ACCOUNT_LINKING,
|
||||
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT:
|
||||
process.env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT,
|
||||
AUTH_DISABLE_USERNAME_PASSWORD: process.env.AUTH_DISABLE_USERNAME_PASSWORD,
|
||||
|
||||
@@ -137,50 +137,6 @@ export const datasetRouter = createTRPCRouter({
|
||||
}
|
||||
>
|
||||
>(Prisma.sql`
|
||||
WITH avg_scores_by_run_id AS (
|
||||
SELECT
|
||||
ri.dataset_run_id run_id,
|
||||
s.name score_name,
|
||||
AVG(s.value) AS average_score_value
|
||||
FROM
|
||||
dataset_run_items ri
|
||||
JOIN scores s
|
||||
ON s.trace_id = ri.trace_id
|
||||
AND (ri.observation_id IS NULL OR s.observation_id = ri.observation_id) -- only include scores that are linked to the observation if observation is linked
|
||||
AND s.project_id = ${input.projectId}
|
||||
JOIN traces t ON t.id = s.trace_id
|
||||
WHERE t.project_id = ${input.projectId}
|
||||
GROUP BY
|
||||
ri.dataset_run_id,
|
||||
s.name
|
||||
ORDER BY
|
||||
1,
|
||||
2
|
||||
),
|
||||
json_avg_scores_by_run_id AS (
|
||||
SELECT
|
||||
run_id,
|
||||
jsonb_object_agg(score_name,
|
||||
average_score_value) AS scores
|
||||
FROM
|
||||
avg_scores_by_run_id
|
||||
GROUP BY
|
||||
run_id
|
||||
ORDER BY
|
||||
run_id
|
||||
),
|
||||
latency_and_total_cost_by_run_id AS (
|
||||
SELECT
|
||||
ri.dataset_run_id run_id,
|
||||
AVG(CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END) AS "avgLatency",
|
||||
AVG(COALESCE(o.calculated_total_cost, 0)) AS "avgTotalCost"
|
||||
FROM
|
||||
dataset_run_items ri
|
||||
JOIN observations_view o ON o.id = ri.observation_id
|
||||
WHERE o.project_id = ${input.projectId}
|
||||
group by 1
|
||||
)
|
||||
|
||||
SELECT
|
||||
runs.id,
|
||||
runs.name,
|
||||
@@ -188,28 +144,52 @@ export const datasetRouter = createTRPCRouter({
|
||||
runs.metadata,
|
||||
runs.created_at "createdAt",
|
||||
runs.updated_at "updatedAt",
|
||||
COALESCE(avg_scores.scores, '[]'::jsonb) scores,
|
||||
COALESCE(avg_scores.scores, '{}') scores,
|
||||
COALESCE(latency_and_total_cost."avgLatency", 0) "avgLatency",
|
||||
COALESCE(latency_and_total_cost."avgTotalCost", 0) "avgTotalCost",
|
||||
count(DISTINCT ri.id)::int "countRunItems"
|
||||
COALESCE(run_items_count.count, 0)::int "countRunItems"
|
||||
FROM
|
||||
dataset_runs runs
|
||||
JOIN datasets ON datasets.id = runs.dataset_id
|
||||
LEFT JOIN dataset_run_items ri ON ri.dataset_run_id = runs.id
|
||||
LEFT JOIN json_avg_scores_by_run_id avg_scores ON avg_scores.run_id = runs.id
|
||||
LEFT JOIN latency_and_total_cost_by_run_id latency_and_total_cost ON latency_and_total_cost.run_id = runs.id
|
||||
WHERE runs.dataset_id = ${input.datasetId}
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
jsonb_object_agg(s.name, s.avg_value) AS scores
|
||||
FROM (
|
||||
SELECT
|
||||
s.name,
|
||||
AVG(s.value) AS avg_value
|
||||
FROM
|
||||
dataset_run_items ri
|
||||
JOIN scores s
|
||||
ON s.trace_id = ri.trace_id
|
||||
AND (ri.observation_id IS NULL OR s.observation_id = ri.observation_id)
|
||||
AND s.project_id = ${input.projectId}
|
||||
JOIN traces t ON t.id = s.trace_id
|
||||
WHERE
|
||||
t.project_id = ${input.projectId}
|
||||
AND ri.dataset_run_id = runs.id
|
||||
GROUP BY s.name
|
||||
) s
|
||||
) avg_scores ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
AVG(o.latency) AS "avgLatency",
|
||||
AVG(COALESCE(o.calculated_total_cost, 0)) AS "avgTotalCost"
|
||||
FROM
|
||||
dataset_run_items ri
|
||||
JOIN observations_view o ON o.id = ri.observation_id
|
||||
WHERE
|
||||
o.project_id = ${input.projectId}
|
||||
AND ri.dataset_run_id = runs.id
|
||||
) latency_and_total_cost ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT count(*) as count
|
||||
FROM dataset_run_items ri
|
||||
WHERE ri.dataset_run_id = runs.id
|
||||
) run_items_count ON true
|
||||
WHERE
|
||||
runs.dataset_id = ${input.datasetId}
|
||||
AND datasets.project_id = ${input.projectId}
|
||||
GROUP BY
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9
|
||||
ORDER BY
|
||||
runs.created_at DESC
|
||||
LIMIT ${input.limit}
|
||||
@@ -550,7 +530,6 @@ export const datasetRouter = createTRPCRouter({
|
||||
},
|
||||
},
|
||||
include: {
|
||||
datasetItem: true,
|
||||
observation: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -11,7 +11,7 @@ const langfuseUrls = {
|
||||
STAGING: "https://staging.langfuse.com",
|
||||
};
|
||||
|
||||
const authUrl =
|
||||
const getAuthURL = () =>
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "US" ||
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU" ||
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
|
||||
@@ -40,7 +40,7 @@ export const sendProjectInvitation = async (
|
||||
invitedByUserEmail: inviterEmail,
|
||||
projectName: projectName,
|
||||
recieverEmail: to,
|
||||
inviteLink: authUrl,
|
||||
inviteLink: getAuthURL(),
|
||||
langfuseCloudRegion: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -118,9 +118,9 @@ export function MultiSelect({
|
||||
>
|
||||
<Check className={cn("h-4 w-4")} />
|
||||
</div>
|
||||
<span>{option.value}</span>
|
||||
<span className="overflow-x-scroll">{option.value}</span>
|
||||
{option.count !== undefined ? (
|
||||
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
|
||||
<span className="ml-auto flex h-4 w-4 items-center justify-center pl-1 font-mono text-xs">
|
||||
{option.count}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { ModelProvider } from "@langfuse/shared";
|
||||
|
||||
export const ZodModelProvider = z.enum([
|
||||
ModelProvider.Anthropic,
|
||||
ModelProvider.OpenAI,
|
||||
]);
|
||||
|
||||
export const CreateLlmApiKey = z.object({
|
||||
projectId: z.string(),
|
||||
secretKey: z.string().min(1),
|
||||
provider: z.literal(ModelProvider.OpenAI),
|
||||
provider: z.nativeEnum(ModelProvider),
|
||||
});
|
||||
|
||||
@@ -77,13 +77,13 @@ export function ManualScoreButton({
|
||||
utils.sessions.invalidate(),
|
||||
]);
|
||||
};
|
||||
const mutCreateScore = api.scores.createReviewScore.useMutation({
|
||||
const mutCreateScore = api.scores.createAnnotationScore.useMutation({
|
||||
onSuccess,
|
||||
});
|
||||
const mutUpdateScore = api.scores.updateReviewScore.useMutation({
|
||||
const mutUpdateScore = api.scores.updateAnnotationScore.useMutation({
|
||||
onSuccess,
|
||||
});
|
||||
const mutDeleteScore = api.scores.deleteReviewScore.useMutation({
|
||||
const mutDeleteScore = api.scores.deleteAnnotationScore.useMutation({
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { AlertTriangle, X } from "lucide-react";
|
||||
import {
|
||||
chatAvailable,
|
||||
sendUserChatMessage,
|
||||
} from "@/src/features/support-chat/chat";
|
||||
|
||||
interface ErrorNotificationProps {
|
||||
error: string;
|
||||
description: string;
|
||||
cause?: string;
|
||||
dismissToast: (t?: string | number | undefined) => void;
|
||||
toast: string | number;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export const ErrorNotification: React.FC<ErrorNotificationProps> = ({
|
||||
error,
|
||||
description,
|
||||
cause,
|
||||
dismissToast,
|
||||
toast,
|
||||
path,
|
||||
}) => {
|
||||
const handleReportIssueClick = () => {
|
||||
if (chatAvailable) {
|
||||
const currentUrl = window.location.href;
|
||||
const message = `I received the following error:\n\nError: ${error}\nDescription: ${description}\n ${cause ? `Cause: ${cause}\n` : ""}${path ? `Path: ${path}\n` : ""}URL: ${currentUrl}`;
|
||||
sendUserChatMessage(message);
|
||||
dismissToast(toast);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
<div className="flex min-w-[300px] flex-1 flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle size={20} className="text-destructive-foreground" />
|
||||
<div className="m-0 text-sm font-medium leading-tight text-destructive-foreground">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
{description && (
|
||||
<div className="text-sm leading-tight text-destructive-foreground">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
{path && (
|
||||
<div className="text-sm leading-tight text-destructive-foreground">
|
||||
Path: {path}
|
||||
</div>
|
||||
)}
|
||||
{cause && (
|
||||
<div className="max-h-32 overflow-y-auto text-sm leading-tight text-destructive-foreground">
|
||||
{cause}
|
||||
</div>
|
||||
)}
|
||||
{chatAvailable && (
|
||||
<Button
|
||||
variant="errorNotification"
|
||||
size={"sm"}
|
||||
onClick={() => {
|
||||
handleReportIssueClick();
|
||||
}}
|
||||
>
|
||||
Report issue to Langfuse team
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="flex h-6 w-6 cursor-pointer items-start justify-end border-none bg-transparent p-0 text-destructive-foreground transition-colors duration-200"
|
||||
onClick={() => dismissToast(toast)}
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -15,7 +15,7 @@ interface NotificationProps {
|
||||
toast: string | number;
|
||||
}
|
||||
|
||||
const Notification: React.FC<NotificationProps> = ({
|
||||
export const Notification: React.FC<NotificationProps> = ({
|
||||
notification,
|
||||
setLastSeenId,
|
||||
dismissToast,
|
||||
@@ -50,5 +50,3 @@ const Notification: React.FC<NotificationProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notification;
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import useLocalStorage from "@/src/components/useLocalStorage";
|
||||
import Notification, {
|
||||
import {
|
||||
Notification,
|
||||
type TNotification,
|
||||
} from "@/src/features/notifications/Notification";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { toast } from "sonner";
|
||||
import { ErrorNotification } from "@/src/features/notifications/ErrorNotification";
|
||||
|
||||
export const showErrorToast = (
|
||||
error: string,
|
||||
description: string,
|
||||
cause?: string,
|
||||
path?: string,
|
||||
) => {
|
||||
toast.custom(
|
||||
(t) => (
|
||||
<ErrorNotification
|
||||
error={error}
|
||||
description={description}
|
||||
cause={cause}
|
||||
path={path}
|
||||
dismissToast={toast.dismiss}
|
||||
toast={t}
|
||||
/>
|
||||
),
|
||||
{
|
||||
duration: Infinity,
|
||||
style: {
|
||||
padding: "1rem",
|
||||
border: "1px solid hsl(var(--destructive))",
|
||||
borderRadius: "0.5rem",
|
||||
backgroundColor: "hsl(var(--destructive))",
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -91,6 +91,7 @@ const events = {
|
||||
"create_new_button_click",
|
||||
],
|
||||
onboarding: ["code_example_tab_switch"],
|
||||
user_settings: ["theme_changed"],
|
||||
project_settings: [
|
||||
"project_delete",
|
||||
"rename_form_submit",
|
||||
|
||||
@@ -169,6 +169,7 @@ export const PromptDetail = () => {
|
||||
promptName={prompt.name}
|
||||
tags={prompt.tags}
|
||||
availableTags={allTags}
|
||||
className="flex-wrap"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,7 +59,7 @@ export function LlmApiKeyList(props: { projectId: string }) {
|
||||
projectId: props.projectId,
|
||||
},
|
||||
{
|
||||
enabled: hasAccess,
|
||||
enabled: hasAccess && env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -175,7 +175,7 @@ function DeleteApiKeyButton(props: { projectId: string; apiKeyId: string }) {
|
||||
|
||||
const formSchema = z.object({
|
||||
secretKey: z.string().min(1),
|
||||
provider: z.literal(ModelProvider.OpenAI),
|
||||
provider: z.nativeEnum(ModelProvider),
|
||||
});
|
||||
|
||||
export function CreateLlmApiKeyComponent(props: {
|
||||
@@ -277,7 +277,7 @@ export function CreateLlmApiKeyComponent(props: {
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value as ModelProvider[number])
|
||||
field.onChange(value as ModelProvider)
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -286,13 +286,7 @@ export function CreateLlmApiKeyComponent(props: {
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{Array.from(
|
||||
new Set(
|
||||
evalLLMModels.map(
|
||||
(models) => models.provider.value,
|
||||
),
|
||||
),
|
||||
).map((provider) => (
|
||||
{Object.values(ModelProvider).map((provider) => (
|
||||
<SelectItem value={provider} key={provider}>
|
||||
{provider}
|
||||
</SelectItem>
|
||||
|
||||
@@ -18,6 +18,7 @@ type TagManagerProps = {
|
||||
hasAccess: boolean;
|
||||
isLoading: boolean;
|
||||
mutateTags: (value: string[]) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const TagManager = ({
|
||||
@@ -26,6 +27,7 @@ const TagManager = ({
|
||||
hasAccess,
|
||||
isLoading,
|
||||
mutateTags,
|
||||
className,
|
||||
}: TagManagerProps) => {
|
||||
const {
|
||||
selectedTags,
|
||||
@@ -59,7 +61,7 @@ const TagManager = ({
|
||||
return (
|
||||
<Popover onOpenChange={(open) => handlePopoverChange(open)}>
|
||||
<PopoverTrigger className="select-none" asChild>
|
||||
<div className="flex gap-x-2 gap-y-1">
|
||||
<div className={cn("flex gap-x-2 gap-y-1", className)}>
|
||||
<TagList selectedTags={selectedTags} isLoading={isLoading} />
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -3,12 +3,14 @@ import { api } from "@/src/utils/api";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import TagManager from "@/src/features/tag/components/TagMananger";
|
||||
import { trpcErrorToast } from "@/src/utils/trpcErrorToast";
|
||||
|
||||
type TagPromptDetailsPopoverProps = {
|
||||
tags: string[];
|
||||
availableTags: string[];
|
||||
projectId: string;
|
||||
promptName: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function TagPromptDetailsPopover({
|
||||
@@ -16,6 +18,7 @@ export function TagPromptDetailsPopover({
|
||||
availableTags,
|
||||
projectId,
|
||||
promptName,
|
||||
className,
|
||||
}: TagPromptDetailsPopoverProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:tag" });
|
||||
@@ -34,6 +37,7 @@ export function TagPromptDetailsPopover({
|
||||
},
|
||||
onError: (err, _newTags, context) => {
|
||||
setIsLoading(false);
|
||||
trpcErrorToast(err);
|
||||
// Rollback to the previous value if mutation fails
|
||||
utils.prompts.allVersions.setData(
|
||||
{ projectId: projectId, name: promptName },
|
||||
@@ -78,6 +82,7 @@ export function TagPromptDetailsPopover({
|
||||
hasAccess={hasAccess}
|
||||
isLoading={isLoading}
|
||||
mutateTags={mutateTags}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { api } from "@/src/utils/api";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
|
||||
import TagManager from "@/src/features/tag/components/TagMananger";
|
||||
import { trpcErrorToast } from "@/src/utils/trpcErrorToast";
|
||||
|
||||
type TagPromptPopverProps = {
|
||||
tags: string[];
|
||||
@@ -10,6 +11,7 @@ type TagPromptPopverProps = {
|
||||
projectId: string;
|
||||
promptName: string;
|
||||
promptsFilter: RouterInput["prompts"]["all"];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function TagPromptPopover({
|
||||
@@ -18,6 +20,7 @@ export function TagPromptPopover({
|
||||
projectId,
|
||||
promptName,
|
||||
promptsFilter,
|
||||
className,
|
||||
}: TagPromptPopverProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:tag" });
|
||||
@@ -32,7 +35,7 @@ export function TagPromptPopover({
|
||||
},
|
||||
onError: (err, _newTags, context) => {
|
||||
utils.prompts.all.setData(promptsFilter, context?.prevPrompt);
|
||||
console.error(err);
|
||||
trpcErrorToast(err);
|
||||
setIsLoading(false);
|
||||
},
|
||||
onSettled: (data, error, { name, tags }) => {
|
||||
@@ -66,6 +69,7 @@ export function TagPromptPopover({
|
||||
hasAccess={hasAccess}
|
||||
isLoading={isLoading}
|
||||
mutateTags={mutateTags}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ import { api } from "@/src/utils/api";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import TagManager from "@/src/features/tag/components/TagMananger";
|
||||
import { trpcErrorToast } from "@/src/utils/trpcErrorToast";
|
||||
|
||||
type TagTraceDetailsPopoverProps = {
|
||||
tags: string[];
|
||||
availableTags: string[];
|
||||
projectId: string;
|
||||
traceId: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function TagTraceDetailsPopover({
|
||||
@@ -16,6 +18,7 @@ export function TagTraceDetailsPopover({
|
||||
availableTags,
|
||||
projectId,
|
||||
traceId,
|
||||
className,
|
||||
}: TagTraceDetailsPopoverProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:tag" });
|
||||
@@ -32,6 +35,7 @@ export function TagTraceDetailsPopover({
|
||||
},
|
||||
onError: (err, _newTags, context) => {
|
||||
setIsLoading(false);
|
||||
trpcErrorToast(err);
|
||||
// Rollback to the previous value if mutation fails
|
||||
utils.traces.byId.setData({ traceId }, context?.prev);
|
||||
},
|
||||
@@ -69,6 +73,7 @@ export function TagTraceDetailsPopover({
|
||||
hasAccess={hasAccess}
|
||||
isLoading={isLoading}
|
||||
mutateTags={mutateTags}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { api } from "@/src/utils/api";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
|
||||
import TagManager from "@/src/features/tag/components/TagMananger";
|
||||
import { trpcErrorToast } from "@/src/utils/trpcErrorToast";
|
||||
|
||||
type TagTracePopoverProps = {
|
||||
tags: string[];
|
||||
@@ -10,6 +11,7 @@ type TagTracePopoverProps = {
|
||||
projectId: string;
|
||||
traceId: string;
|
||||
tracesFilter: RouterInput["traces"]["all"];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function TagTracePopover({
|
||||
@@ -18,6 +20,7 @@ export function TagTracePopover({
|
||||
projectId,
|
||||
traceId,
|
||||
tracesFilter,
|
||||
className,
|
||||
}: TagTracePopoverProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:tag" });
|
||||
@@ -32,7 +35,7 @@ export function TagTracePopover({
|
||||
},
|
||||
onError: (err, _newTags, context) => {
|
||||
utils.traces.all.setData(tracesFilter, context?.prevTrace);
|
||||
console.log("error", err);
|
||||
trpcErrorToast(err);
|
||||
setIsLoading(false);
|
||||
},
|
||||
onSettled: (data, error, { traceId, tags }) => {
|
||||
@@ -68,6 +71,7 @@ export function TagTracePopover({
|
||||
hasAccess={hasAccess}
|
||||
isLoading={isLoading}
|
||||
mutateTags={mutateTags}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ import * as React from "react";
|
||||
import { Monitor, Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const capture = usePostHogClientCapture();
|
||||
return (
|
||||
<div className="ml-auto flex items-center space-x-1">
|
||||
<div title="Light mode">
|
||||
@@ -17,6 +18,9 @@ export function ThemeToggle() {
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setTheme("light");
|
||||
capture("user_settings:theme_changed", {
|
||||
theme: "light",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -29,6 +33,9 @@ export function ThemeToggle() {
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setTheme("dark");
|
||||
capture("user_settings:theme_changed", {
|
||||
theme: "dark",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -41,6 +48,9 @@ export function ThemeToggle() {
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setTheme("system");
|
||||
capture("user_settings:theme_changed", {
|
||||
theme: "system",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ export const ProjectUsageChart: React.FC<{ projectId: string }> = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header title="Usage" level="h3" />
|
||||
<Header title="Usage & Billing" level="h3" />
|
||||
<Card className="p-4 lg:w-1/2">
|
||||
{usage.data !== undefined && (
|
||||
<>
|
||||
@@ -81,7 +81,7 @@ export const ProjectUsageChart: React.FC<{ projectId: string }> = ({
|
||||
level="h3"
|
||||
actionButtons={
|
||||
<Button variant="secondary" asChild>
|
||||
<Link href="https://langfuse.com/pricing">
|
||||
<Link href="https://langfuse.com/pricing" target="_blank">
|
||||
Pricing page ↗
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -98,6 +98,11 @@ export const ProjectUsageChart: React.FC<{ projectId: string }> = ({
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="secondary" asChild>
|
||||
<Link href="https://langfuse.com/pricing" target="_blank">
|
||||
Pricing page ↗
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="inline-block text-sm text-muted-foreground">
|
||||
Current plan: {plan}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import chatCompletionHandler from "@/src/ee/features/playground/server/chatCompletionHandler";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export default chatCompletionHandler;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { ServerPosthog } from "@/src/features/posthog-analytics/ServerPosthog";
|
||||
import { prisma, Prisma } from "@langfuse/shared/src/db";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
@@ -40,231 +40,6 @@ export default async function handler(
|
||||
)?.lastRun ?? undefined;
|
||||
const endTimeframe = new Date(Date.now());
|
||||
|
||||
// New/updated projects
|
||||
const projects = await prisma.project.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
projectMembers: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
role: "OWNER",
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
updatedAt: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
projects.forEach((project) => {
|
||||
posthog.groupIdentify({
|
||||
groupType: "project",
|
||||
groupKey: project.id,
|
||||
properties: {
|
||||
project_name: project.name,
|
||||
project_owner: project.projectMembers
|
||||
.map((member) => member.user.email)
|
||||
.join(","),
|
||||
created_at: project.createdAt,
|
||||
environment: process.env.NODE_ENV,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// traces
|
||||
const traceCountPerProject = await prisma.trace.groupBy({
|
||||
by: ["projectId"],
|
||||
where: {
|
||||
timestamp: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
traceCountPerProject.forEach((value) => {
|
||||
posthog.capture({
|
||||
event: "ingestion_metrics",
|
||||
distinctId: posthog_event_user_id,
|
||||
groups: {
|
||||
project: value.projectId,
|
||||
},
|
||||
properties: {
|
||||
traces: value._count.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// datasets
|
||||
const datasetCountPerProject = await prisma.dataset.groupBy({
|
||||
by: ["projectId"],
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
datasetCountPerProject.forEach((value) => {
|
||||
posthog.capture({
|
||||
event: "ingestion_metrics",
|
||||
distinctId: posthog_event_user_id,
|
||||
groups: {
|
||||
project: value.projectId,
|
||||
},
|
||||
properties: {
|
||||
datasets: value._count.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const datasetItemsPerProject = await prisma.$queryRaw<
|
||||
Array<{
|
||||
project_id: string;
|
||||
count_dataset_items: number;
|
||||
}>
|
||||
>`
|
||||
SELECT
|
||||
datasets.project_id project_id,
|
||||
count(DISTINCT item.id)::integer count_dataset_items
|
||||
FROM
|
||||
dataset_items item
|
||||
JOIN datasets ON datasets.id = item.dataset_id
|
||||
WHERE
|
||||
item.created_at < ${endTimeframe}
|
||||
${
|
||||
startTimeframe
|
||||
? Prisma.sql`AND item.created_at >= ${startTimeframe}`
|
||||
: Prisma.empty
|
||||
}
|
||||
GROUP BY
|
||||
1
|
||||
`;
|
||||
datasetItemsPerProject.forEach((value) => {
|
||||
posthog.capture({
|
||||
event: "ingestion_metrics",
|
||||
distinctId: posthog_event_user_id,
|
||||
groups: {
|
||||
project: value.project_id,
|
||||
},
|
||||
properties: {
|
||||
dataset_items: value.count_dataset_items,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const datasetRunItemsPerProject = await prisma.$queryRaw<
|
||||
Array<{
|
||||
project_id: string;
|
||||
count_dataset_run_items: number;
|
||||
}>
|
||||
>`
|
||||
SELECT
|
||||
datasets.project_id project_id,
|
||||
count(DISTINCT run_item.id)::integer count_dataset_run_items
|
||||
FROM
|
||||
dataset_run_items run_item
|
||||
JOIN dataset_runs run ON run.id = run_item.dataset_run_id
|
||||
JOIN datasets ON datasets.id = run.dataset_id
|
||||
WHERE
|
||||
run_item.created_at < ${endTimeframe}
|
||||
${
|
||||
startTimeframe
|
||||
? Prisma.sql`AND run_item.created_at >= ${startTimeframe}`
|
||||
: Prisma.empty
|
||||
}
|
||||
GROUP BY
|
||||
1
|
||||
`;
|
||||
datasetRunItemsPerProject.forEach((value) => {
|
||||
posthog.capture({
|
||||
event: "ingestion_metrics",
|
||||
distinctId: posthog_event_user_id,
|
||||
groups: {
|
||||
project: value.project_id,
|
||||
},
|
||||
properties: {
|
||||
dataset_run_items: value.count_dataset_run_items,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// scores
|
||||
const scoreCountPerProject = await prisma.$queryRaw<
|
||||
Array<{
|
||||
project_id: string;
|
||||
count_scores: number;
|
||||
}>
|
||||
>`
|
||||
SELECT
|
||||
s.project_id,
|
||||
count(s.*)::integer count_scores
|
||||
FROM
|
||||
scores s
|
||||
WHERE
|
||||
s.timestamp < ${endTimeframe}
|
||||
${
|
||||
startTimeframe
|
||||
? Prisma.sql`AND s.timestamp >= ${startTimeframe}`
|
||||
: Prisma.empty
|
||||
}
|
||||
GROUP BY
|
||||
1
|
||||
`;
|
||||
scoreCountPerProject.forEach((value) => {
|
||||
posthog.capture({
|
||||
event: "ingestion_metrics",
|
||||
distinctId: posthog_event_user_id,
|
||||
groups: {
|
||||
project: value.project_id,
|
||||
},
|
||||
properties: {
|
||||
scores: value.count_scores,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// observations
|
||||
const observationCountPerProject = await prisma.observation.groupBy({
|
||||
by: ["projectId"],
|
||||
where: {
|
||||
startTime: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
observationCountPerProject.forEach((value) => {
|
||||
posthog.capture({
|
||||
event: "ingestion_metrics",
|
||||
distinctId: posthog_event_user_id,
|
||||
groups: {
|
||||
project: value.projectId,
|
||||
},
|
||||
properties: {
|
||||
observations: value._count.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// db size
|
||||
const dbSize = await prisma.$queryRaw<
|
||||
Array<{
|
||||
@@ -290,13 +65,6 @@ export default async function handler(
|
||||
"to endTimeframe:",
|
||||
endTimeframe.toISOString(),
|
||||
{
|
||||
"#projects with traces": traceCountPerProject.length,
|
||||
"#projects with observations": observationCountPerProject.length,
|
||||
"#projects with scores": scoreCountPerProject.length,
|
||||
"#projects (new/updated)": projects.length,
|
||||
"#projects with datasets": datasetCountPerProject.length,
|
||||
"#projects with dataset items": datasetItemsPerProject.length,
|
||||
"#projects with dataset run items": datasetRunItemsPerProject.length,
|
||||
"db size in MB": dbSize[0]?.size_in_mb,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -258,15 +258,21 @@ const handleSingleEvent = async (
|
||||
req: NextApiRequest,
|
||||
apiScope: ApiAccessScope,
|
||||
) => {
|
||||
if ("body" in event && "input" in event.body && "output" in event.body) {
|
||||
const { body } = event;
|
||||
let restEvent = body;
|
||||
if ("input" in body) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { input, output, ...restEvent } = event.body;
|
||||
console.log(
|
||||
`handling single event ${event.id} ${JSON.stringify({ event, body: restEvent })}`,
|
||||
);
|
||||
} else {
|
||||
console.log(`handling single event ${event.id} ${JSON.stringify(event)}`);
|
||||
const { input, ...rest } = body;
|
||||
restEvent = rest;
|
||||
}
|
||||
if ("output" in restEvent) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { output, ...rest } = restEvent;
|
||||
restEvent = rest;
|
||||
}
|
||||
console.log(
|
||||
`handling single event ${event.id} ${JSON.stringify({ body: restEvent })}`,
|
||||
);
|
||||
|
||||
const cleanedEvent = ingestionEvent.parse(cleanEvent(event));
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { env } from "@/src/env.mjs";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
import { FaGithub } from "react-icons/fa";
|
||||
import { SiOkta, SiAuth0 } from "react-icons/si";
|
||||
import { SiOkta, SiAuth0, SiAmazoncognito } from "react-icons/si";
|
||||
import { TbBrandAzure } from "react-icons/tb";
|
||||
import { signIn } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
@@ -49,6 +49,7 @@ export type PageProps = {
|
||||
okta: boolean;
|
||||
azureAd: boolean;
|
||||
auth0: boolean;
|
||||
cognito: boolean;
|
||||
sso: boolean;
|
||||
};
|
||||
signUpDisabled: boolean;
|
||||
@@ -80,6 +81,10 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async () => {
|
||||
env.AUTH_AUTH0_CLIENT_ID !== undefined &&
|
||||
env.AUTH_AUTH0_CLIENT_SECRET !== undefined &&
|
||||
env.AUTH_AUTH0_ISSUER !== undefined,
|
||||
cognito:
|
||||
env.AUTH_COGNITO_CLIENT_ID !== undefined &&
|
||||
env.AUTH_COGNITO_CLIENT_SECRET !== undefined &&
|
||||
env.AUTH_COGNITO_ISSUER !== undefined,
|
||||
sso,
|
||||
},
|
||||
signUpDisabled: env.AUTH_DISABLE_SIGNUP === "true",
|
||||
@@ -169,6 +174,18 @@ export function SSOButtons({
|
||||
Auth0
|
||||
</Button>
|
||||
)}
|
||||
{authProviders.cognito && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
capture("sign_in:button_click", { provider: "cognito" });
|
||||
void signIn("cognito");
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
<SiAmazoncognito className="mr-3" size={18} />
|
||||
Cognito
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
@@ -421,7 +438,7 @@ export default function SignIn({ authProviders, signUpDisabled }: PageProps) {
|
||||
No account yet?{" "}
|
||||
<Link
|
||||
href="/auth/sign-up"
|
||||
className="hover:text-hover-primary-accent font-semibold leading-6 text-primary-accent"
|
||||
className="font-semibold leading-6 text-primary-accent hover:text-hover-primary-accent"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
type OptionsDefinition,
|
||||
type ColumnDefinition,
|
||||
ScoreSource,
|
||||
} from "@langfuse/shared";
|
||||
|
||||
export const scoresTableCols: ColumnDefinition[] = [
|
||||
@@ -31,8 +32,9 @@ export const scoresTableCols: ColumnDefinition[] = [
|
||||
{
|
||||
name: "Source",
|
||||
id: "source",
|
||||
type: "string",
|
||||
internal: 's."source"',
|
||||
type: "stringOptions",
|
||||
internal: 's."source"::text',
|
||||
options: Object.values(ScoreSource).map((value) => ({ value })),
|
||||
},
|
||||
{
|
||||
name: "Name",
|
||||
|
||||
@@ -121,7 +121,7 @@ export const scoresRouter = createTRPCRouter({
|
||||
|
||||
return res;
|
||||
}),
|
||||
createReviewScore: protectedProjectProcedure
|
||||
createAnnotationScore: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
@@ -158,7 +158,7 @@ export const scoresRouter = createTRPCRouter({
|
||||
name: input.name,
|
||||
comment: input.comment,
|
||||
authorUserId: ctx.session.user.id,
|
||||
source: "REVIEW",
|
||||
source: "ANNOTATION",
|
||||
},
|
||||
});
|
||||
await auditLog({
|
||||
@@ -174,7 +174,7 @@ export const scoresRouter = createTRPCRouter({
|
||||
});
|
||||
return score;
|
||||
}),
|
||||
updateReviewScore: protectedProjectProcedure
|
||||
updateAnnotationScore: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
@@ -193,11 +193,11 @@ export const scoresRouter = createTRPCRouter({
|
||||
where: {
|
||||
id: input.id,
|
||||
projectId: input.projectId,
|
||||
source: "REVIEW",
|
||||
source: "ANNOTATION",
|
||||
},
|
||||
});
|
||||
if (!score) {
|
||||
throw new Error("No review score with this id in this project.");
|
||||
throw new Error("No annotation score with this id in this project.");
|
||||
}
|
||||
|
||||
await auditLog({
|
||||
@@ -224,7 +224,7 @@ export const scoresRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
}),
|
||||
deleteReviewScore: protectedProjectProcedure
|
||||
deleteAnnotationScore: protectedProjectProcedure
|
||||
.input(z.object({ projectId: z.string(), id: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
throwIfNoAccess({
|
||||
@@ -236,12 +236,12 @@ export const scoresRouter = createTRPCRouter({
|
||||
const score = await ctx.prisma.score.findFirst({
|
||||
where: {
|
||||
id: input.id,
|
||||
source: "REVIEW",
|
||||
source: "ANNOTATION",
|
||||
projectId: input.projectId,
|
||||
},
|
||||
});
|
||||
if (!score) {
|
||||
throw new Error("No review score with this id in this project.");
|
||||
throw new Error("No annotation score with this id in this project.");
|
||||
}
|
||||
|
||||
await auditLog({
|
||||
|
||||
@@ -79,6 +79,7 @@ import superjson from "superjson";
|
||||
import { ZodError } from "zod";
|
||||
import { setUpSuperjson } from "@/src/utils/superjson";
|
||||
import { DB } from "@/src/server/db";
|
||||
import { isProjectMemberOrAdmin } from "@/src/server/utils/checkProjectMembershipOrAdmin";
|
||||
|
||||
setUpSuperjson();
|
||||
|
||||
@@ -177,7 +178,7 @@ const enforceUserIsAuthedAndProjectMember = t.middleware(
|
||||
({ id }) => id === projectId,
|
||||
);
|
||||
|
||||
if (!sessionProject && ctx.session.user.admin !== true)
|
||||
if (!sessionProject && !isProjectMemberOrAdmin(ctx.session.user, projectId))
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "User is not a member of this project",
|
||||
@@ -242,7 +243,11 @@ const enforceTraceAccess = t.middleware(async ({ ctx, rawInput, next }) => {
|
||||
({ id }) => id === trace.projectId,
|
||||
);
|
||||
|
||||
if (!trace.public && !sessionProject && ctx.session?.user?.admin !== true)
|
||||
if (
|
||||
!trace.public &&
|
||||
!sessionProject &&
|
||||
!isProjectMemberOrAdmin(ctx.session?.user, trace.projectId)
|
||||
)
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message:
|
||||
|
||||
+23
-7
@@ -19,9 +19,10 @@ import GoogleProvider from "next-auth/providers/google";
|
||||
import GitHubProvider from "next-auth/providers/github";
|
||||
import OktaProvider from "next-auth/providers/okta";
|
||||
import Auth0Provider from "next-auth/providers/auth0";
|
||||
import CognitoProvider from "next-auth/providers/cognito";
|
||||
import AzureADProvider from "next-auth/providers/azure-ad";
|
||||
import { type Provider } from "next-auth/providers/index";
|
||||
import { getCookieName, cookieOptions } from "./utils/cookies";
|
||||
import { getCookieName, getCookieOptions } from "./utils/cookies";
|
||||
import {
|
||||
getSsoAuthProviderIdForDomain,
|
||||
loadSsoProviders,
|
||||
@@ -182,6 +183,21 @@ if (
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
env.AUTH_COGNITO_CLIENT_ID &&
|
||||
env.AUTH_COGNITO_CLIENT_SECRET &&
|
||||
env.AUTH_COGNITO_ISSUER
|
||||
)
|
||||
staticProviders.push(
|
||||
CognitoProvider({
|
||||
clientId: env.AUTH_COGNITO_CLIENT_ID,
|
||||
clientSecret: env.AUTH_COGNITO_CLIENT_SECRET,
|
||||
issuer: env.AUTH_COGNITO_ISSUER,
|
||||
allowDangerousEmailAccountLinking:
|
||||
env.AUTH_COGNITO_ALLOW_ACCOUNT_LINKING === "true",
|
||||
}),
|
||||
);
|
||||
|
||||
// Extend Prisma Adapter
|
||||
const prismaAdapter = PrismaAdapter(prisma);
|
||||
const extendedPrismaAdapter: Adapter = {
|
||||
@@ -314,27 +330,27 @@ export async function getAuthOptions(): Promise<NextAuthOptions> {
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
name: getCookieName("next-auth.session-token"),
|
||||
options: cookieOptions,
|
||||
options: getCookieOptions(),
|
||||
},
|
||||
csrfToken: {
|
||||
name: getCookieName("next-auth.csrf-token"),
|
||||
options: cookieOptions,
|
||||
options: getCookieOptions(),
|
||||
},
|
||||
callbackUrl: {
|
||||
name: getCookieName("next-auth.callback-url"),
|
||||
options: cookieOptions,
|
||||
options: getCookieOptions(),
|
||||
},
|
||||
state: {
|
||||
name: getCookieName("next-auth.state"),
|
||||
options: cookieOptions,
|
||||
options: getCookieOptions(),
|
||||
},
|
||||
nonce: {
|
||||
name: getCookieName("next-auth.nonce"),
|
||||
options: cookieOptions,
|
||||
options: getCookieOptions(),
|
||||
},
|
||||
pkceCodeVerifier: {
|
||||
name: getCookieName("next-auth.pkce.code_verifier"),
|
||||
options: cookieOptions,
|
||||
options: getCookieOptions(),
|
||||
},
|
||||
},
|
||||
events: {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { type User } from "next-auth";
|
||||
|
||||
export const isProjectMemberOrAdmin = (
|
||||
user: User | null | undefined,
|
||||
projectId: string,
|
||||
): boolean => {
|
||||
if (!user) return false;
|
||||
|
||||
const isAdmin = user.admin === true;
|
||||
const isProjectMember = user.projects.some(
|
||||
(project) => project.id === projectId,
|
||||
);
|
||||
|
||||
return isProjectMember || isAdmin;
|
||||
};
|
||||
@@ -1,20 +1,20 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
// Use secure cookies on https hostnames, exception for Vercel which sets NEXTAUTH_URL without the protocol
|
||||
const shouldSecureCookies =
|
||||
const shouldSecureCookies = () =>
|
||||
env.NEXTAUTH_URL.startsWith("https://") || process.env.VERCEL === "1";
|
||||
|
||||
export const cookieOptions = {
|
||||
export const getCookieOptions = () => ({
|
||||
domain: env.NEXTAUTH_COOKIE_DOMAIN ?? undefined,
|
||||
httpOnly: true,
|
||||
sameSite: "lax" as const,
|
||||
path: "/",
|
||||
secure: shouldSecureCookies,
|
||||
};
|
||||
secure: shouldSecureCookies(),
|
||||
});
|
||||
|
||||
export const getCookieName = (name: string) =>
|
||||
[
|
||||
shouldSecureCookies ? "__Secure-" : "",
|
||||
shouldSecureCookies() ? "__Secure-" : "",
|
||||
name,
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
|
||||
? `.${env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user