Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8584f93307 | ||
|
|
08e576f815 | ||
|
|
f64da44b20 | ||
|
|
c720a4d896 | ||
|
|
b59b61a5cf | ||
|
|
3f18e42fef | ||
|
|
31a9be6f1d | ||
|
|
fe6d8a38ec | ||
|
|
d4fecbb2a6 | ||
|
|
d92ba1d26b | ||
|
|
61c2312f12 | ||
|
|
862a1395d5 | ||
|
|
21e9e17e93 | ||
|
|
5b32d4b9a2 | ||
|
|
ea5617d621 | ||
|
|
9c72fddc20 | ||
|
|
548669e546 | ||
|
|
a7a5870508 | ||
|
|
0385a52e2c | ||
|
|
dd3f4824e3 | ||
|
|
aa6145e341 | ||
|
|
a70f1d58a5 | ||
|
|
16870fb489 | ||
|
|
24b8722ad5 | ||
|
|
d0cf2f56a0 | ||
|
|
20e3c25fd6 | ||
|
|
b2be82b436 | ||
|
|
779771950b | ||
|
|
fdfc64b853 | ||
|
|
ca43da7562 | ||
|
|
12a2459a1d | ||
|
|
5c1a7535ee | ||
|
|
32f4e5c64f | ||
|
|
06b8331a07 | ||
|
|
817554f1c9 | ||
|
|
37d982a32f | ||
|
|
1f75563d55 | ||
|
|
f5775500c8 | ||
|
|
97e26dc8f3 | ||
|
|
1d733e39ff | ||
|
|
12abeac07a | ||
|
|
6478456e6c | ||
|
|
5e27d36d74 | ||
|
|
8461a7a305 | ||
|
|
5fd3cd218a | ||
|
|
38cc982924 |
@@ -114,4 +114,10 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
# Crisp chat
|
||||
# NEXT_PUBLIC_CRISP_WEBSITE_ID=
|
||||
|
||||
# Encryption
|
||||
# ENCRYPTION_KEY=
|
||||
|
||||
# Admin API
|
||||
# ADMIN_API_KEY=
|
||||
|
||||
### END Langfuse Cloud Config
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 8.15.5
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 8.15.5
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 8.15.5
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 8.15.5
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 8.15.5
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
||||
+5
-3
@@ -120,9 +120,11 @@ We built a monorepo using [pnpm](https://pnpm.io/motivation) and [turbo](https:/
|
||||
|
||||
- `web`: is the main application package providing Frontend and Backend APIs for Langfuse.
|
||||
- `worker` (no production yet): contains an application for asynchronous processing of tasks. This package is not yet used in production.
|
||||
- `shared`: contains shared code between the above packages.
|
||||
- `config-eslint`: contains eslint configurations which are shared between the above packages.
|
||||
- `config-typescript`: contains typescript configurations which are shared between the above packages.
|
||||
- `packages`:
|
||||
- `shared`: contains shared code between the above packages.
|
||||
- `config-eslint`: contains eslint configurations which are shared between the above packages.
|
||||
- `config-typescript`: contains typescript configurations which are shared between the above packages.
|
||||
- `ee`: contains all enterprise features. See [EE README](ee/README.md) for more details.
|
||||
|
||||
## Development Setup
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ Copyright (c) 2023 Finto Technologies GmbH
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
* All content that resides under the "ee/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE".
|
||||
* All third party components incorporated into the Finto Technologies Software are licensed under the original license provided by the owner of the applicable component.
|
||||
* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
||||
- All content that resides under the "ee/" and/or "web/src/ee" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE".
|
||||
- All third party components incorporated into the Finto Technologies Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -22,4 +22,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
||||
@@ -101,13 +101,13 @@ 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** - _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. |
|
||||
|
||||
External projects/packages that integrate with Langfuse:
|
||||
|
||||
@@ -142,7 +142,7 @@ In order of preference the best way to communicate with us:
|
||||
|
||||
## License
|
||||
|
||||
This repository is MIT licensed, except for the `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
This repository is MIT licensed, except for the `ee` folders. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
## Misc
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["@repo/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
};
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
# Enterprise Edition
|
||||
|
||||
While we are currently working 100% on the open source version, we consider financing the project by developing some premium features as part of an enterprise version under a commercial license. Please reach out to us if you have specific requirements: enterprise@langfuse.com
|
||||
This folder includes features that are only available in the Enterprise Edition of Langfuse and on Langfuse Cloud.
|
||||
|
||||
See [LICENSE](../LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@langfuse/ee",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "./dist/src/index.js",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/src/index.js",
|
||||
"require": "./dist/src/index.js"
|
||||
},
|
||||
"./sso": {
|
||||
"import": "./dist/src/sso/index.js",
|
||||
"require": "./dist/src/sso/index.js"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"next": "^14.2.1",
|
||||
"next-auth": "^4.24.7",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "*",
|
||||
"@repo/typescript-config": "*",
|
||||
"@types/node": "^20.11.29",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.2.0",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 120
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { env } from "../env";
|
||||
|
||||
export const isEeAvailable: boolean =
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { env as sharedEnv } from "@langfuse/shared";
|
||||
|
||||
const EnvSchema = z.object({
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z.string().optional(),
|
||||
ADMIN_API_KEY: z.string().optional(),
|
||||
});
|
||||
|
||||
export const env = { ...sharedEnv, ...EnvSchema.parse(process.env) };
|
||||
@@ -0,0 +1 @@
|
||||
export { isEeAvailable } from "./ee-license-check";
|
||||
@@ -0,0 +1,236 @@
|
||||
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 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 { encrypt, decrypt } from "@langfuse/shared/encryption";
|
||||
import { SsoProviderSchema } from "./types";
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { env } from "../env";
|
||||
|
||||
// Local cache for SSO configurations
|
||||
let cachedSsoConfigs: {
|
||||
data: SsoProviderSchema[];
|
||||
timestamp: number;
|
||||
} | null = null;
|
||||
|
||||
/**
|
||||
* Get all SSO configurations from the database or from local cache and parse them into SsoProviderSchema objects.
|
||||
*
|
||||
* @returns {Promise<SsoProviderSchema[]>} - A list of all SSO configurations. Empty array if none are configured or EE is not available.
|
||||
*/
|
||||
async function getSsoConfigs(): Promise<SsoProviderSchema[]> {
|
||||
if (!isEeAvailable) return [];
|
||||
const CACHE_TTL = 60 * 1000; // 1 minute
|
||||
|
||||
// Set/refresh the cache if it's empty or expired
|
||||
if (
|
||||
cachedSsoConfigs === null ||
|
||||
Date.now() - cachedSsoConfigs.timestamp > CACHE_TTL
|
||||
) {
|
||||
// transform into zod object
|
||||
const dbConfigs = await prisma.ssoConfig.findMany();
|
||||
const parsedSsoConfigs = dbConfigs
|
||||
.map((v) => {
|
||||
try {
|
||||
const parsedValue = SsoProviderSchema.parse(v);
|
||||
return parsedValue;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Failed to parse SSO provider config for domain ${v.domain}`,
|
||||
e
|
||||
);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((parsed) => parsed !== null) as SsoProviderSchema[];
|
||||
|
||||
cachedSsoConfigs = {
|
||||
data: parsedSsoConfigs,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
return cachedSsoConfigs.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all custom SSO providers from the database. To be used within `providers` in NextAuth backend configuration.
|
||||
*
|
||||
* @returns {Promise<Provider[]>} - A list of all custom SSO providers.
|
||||
*/
|
||||
export async function loadSsoProviders(): Promise<Provider[]> {
|
||||
if (!isEeAvailable) return [];
|
||||
|
||||
const ssoConfigs = await getSsoConfigs();
|
||||
|
||||
const providers: Provider[] = [];
|
||||
|
||||
for (const dbSsoConfig of ssoConfigs) {
|
||||
const provider = dbToNextAuthProvider(dbSsoConfig);
|
||||
if (provider !== null) providers.push(provider);
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if any custom SSO provider is configured in the database.
|
||||
*/
|
||||
export async function isAnySsoConfigured(): Promise<boolean> {
|
||||
if (!isEeAvailable) return false;
|
||||
const ssoConfigs = await getSsoConfigs();
|
||||
return ssoConfigs.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the custom SSO providerId for a domain. To be used with NextAuth's `signIn(providerId)`.
|
||||
*
|
||||
* @param domain - The domain to check for a custom SSO provider, e.g. "example.com".
|
||||
* @returns `providerId` or null if none is configured or EE is not available.
|
||||
*/
|
||||
export async function getSsoAuthProviderIdForDomain(
|
||||
domain: string
|
||||
): Promise<string | null> {
|
||||
if (!isEeAvailable) return null;
|
||||
const ssoConfig = (await getSsoConfigs()).find(
|
||||
(ssoConfig) => ssoConfig.domain === domain.toLowerCase()
|
||||
);
|
||||
|
||||
if (!ssoConfig) return null;
|
||||
return getAuthProviderIdForSsoConfig(ssoConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a SsoProviderConfig to a NextAuth Provider instance.
|
||||
*
|
||||
* @param {SsoProviderSchema} provider - The SSO configuration from the database.
|
||||
* @returns {Provider | null} - A NextAuth Provider instance or null if parsing fails or no custom credentials are used for this SSO config.
|
||||
*/
|
||||
const dbToNextAuthProvider = (provider: SsoProviderSchema): Provider | null => {
|
||||
// If the SsoConfig does not use custom credentials, return null as no additional provider needs to be added to NextAuth
|
||||
if (!provider.authConfig) return null;
|
||||
|
||||
if (provider.authProvider === "google")
|
||||
return GoogleProvider({
|
||||
id: getAuthProviderIdForSsoConfig(provider), // use the domain as the provider id as we use domain-specific credentials
|
||||
...provider.authConfig,
|
||||
clientSecret: decrypt(provider.authConfig.clientSecret),
|
||||
});
|
||||
else if (provider.authProvider === "github")
|
||||
return GitHubProvider({
|
||||
id: getAuthProviderIdForSsoConfig(provider), // use the domain as the provider id as we use domain-specific credentials
|
||||
...provider.authConfig,
|
||||
clientSecret: decrypt(provider.authConfig.clientSecret),
|
||||
});
|
||||
else if (provider.authProvider === "auth0")
|
||||
return Auth0Provider({
|
||||
id: getAuthProviderIdForSsoConfig(provider), // use the domain as the provider id as we use domain-specific credentials
|
||||
...provider.authConfig,
|
||||
clientSecret: decrypt(provider.authConfig.clientSecret),
|
||||
});
|
||||
else if (provider.authProvider === "okta")
|
||||
return OktaProvider({
|
||||
id: getAuthProviderIdForSsoConfig(provider), // use the domain as the provider id as we use domain-specific credentials
|
||||
...provider.authConfig,
|
||||
clientSecret: decrypt(provider.authConfig.clientSecret),
|
||||
});
|
||||
else if (provider.authProvider === "azure-ad")
|
||||
return AzureADProvider({
|
||||
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
|
||||
const _: never = provider;
|
||||
throw new Error(
|
||||
`Unrecognized SSO provider for domain ${(provider as any).domain}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the custom SSO providerId for a database SSO configuration. To be used with NextAuth's `signIn(providerId)`.
|
||||
*
|
||||
* @param {DbSsoConfig} dbSsoConfig - The SSO configuration from the database.
|
||||
* @returns {string} - The providerId used in NextAuth.
|
||||
*/
|
||||
const getAuthProviderIdForSsoConfig = (
|
||||
dbSsoConfig: SsoProviderSchema
|
||||
): string => {
|
||||
if (!dbSsoConfig.authConfig) return dbSsoConfig.authProvider;
|
||||
return `${dbSsoConfig.domain}.${dbSsoConfig.authProvider}`;
|
||||
};
|
||||
|
||||
export async function createNewSsoConfigHandler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
try {
|
||||
if (!isEeAvailable) {
|
||||
res.status(403).json({ error: "EE is not available" });
|
||||
return;
|
||||
}
|
||||
// allow only POST requests
|
||||
if (req.method !== "POST") {
|
||||
res.status(405).json({ error: "Method Not Allowed" });
|
||||
return;
|
||||
}
|
||||
// check if ADMIN_API_KEY is set
|
||||
if (!env.ADMIN_API_KEY) {
|
||||
res.status(500).json({ error: "ADMIN_API_KEY is not set" });
|
||||
return;
|
||||
}
|
||||
if (!env.ENCRYPTION_KEY) {
|
||||
res.status(500).json({ error: "ENCRYPTION_KEY is not set" });
|
||||
return;
|
||||
}
|
||||
// check bearer token
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization) {
|
||||
res
|
||||
.status(401)
|
||||
.json({ error: "Unauthorized: No authorization header provided" });
|
||||
return;
|
||||
}
|
||||
const [scheme, token] = authorization.split(" ");
|
||||
if (scheme !== "Bearer" || !token || token !== env.ADMIN_API_KEY) {
|
||||
res.status(401).json({ error: "Unauthorized: Invalid token" });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = SsoProviderSchema.safeParse(req.body);
|
||||
if (!body.success) {
|
||||
res.status(400).json({ error: body.error });
|
||||
return;
|
||||
}
|
||||
|
||||
const { domain, authProvider, authConfig } = body.data;
|
||||
|
||||
const encryptedClientSecret = authConfig
|
||||
? {
|
||||
...authConfig,
|
||||
clientSecret: encrypt(authConfig.clientSecret),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await prisma.ssoConfig.create({
|
||||
data: {
|
||||
domain,
|
||||
authProvider,
|
||||
authConfig: encryptedClientSecret,
|
||||
},
|
||||
});
|
||||
res.status(201).json({
|
||||
message: "SSO configuration created successfully",
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
res.status(500).json({ error: "Internal Server Error" });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const base = z.object({
|
||||
domain: z.string().refine((v) => v === v.toLowerCase(), {
|
||||
message: "Domain must be lowercase",
|
||||
}),
|
||||
});
|
||||
|
||||
export const GoogleProviderSchema = base.extend({
|
||||
authProvider: z.literal("google"),
|
||||
authConfig: z
|
||||
.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
allowDangerousEmailAccountLinking: z.boolean().optional().default(false),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export const GithubProviderSchema = base.extend({
|
||||
authProvider: z.literal("github"),
|
||||
authConfig: z
|
||||
.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
allowDangerousEmailAccountLinking: z.boolean().optional().default(false),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export const Auth0ProviderSchema = base.extend({
|
||||
authProvider: z.literal("auth0"),
|
||||
authConfig: z
|
||||
.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
issuer: z.string(),
|
||||
allowDangerousEmailAccountLinking: z.boolean().optional().default(false),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export const OktaProviderSchema = base.extend({
|
||||
authProvider: z.literal("okta"),
|
||||
authConfig: z
|
||||
.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
issuer: z.string(),
|
||||
allowDangerousEmailAccountLinking: z.boolean().optional().default(false),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export const AzureAdProviderSchema = base.extend({
|
||||
authProvider: z.literal("azure-ad"),
|
||||
authConfig: z
|
||||
.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
tenantId: 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 const SsoProviderSchema = z.discriminatedUnion("authProvider", [
|
||||
GoogleProviderSchema,
|
||||
GithubProviderSchema,
|
||||
Auth0ProviderSchema,
|
||||
OktaProviderSchema,
|
||||
AzureAdProviderSchema,
|
||||
]);
|
||||
|
||||
export type SsoProviderSchema = z.infer<typeof SsoProviderSchema>;
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "NodeNext",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"types": ["node"],
|
||||
"target": "ES2020",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -127,6 +127,7 @@ types:
|
||||
id: string
|
||||
name: string
|
||||
description: optional<string>
|
||||
metadata: optional<unknown>
|
||||
projectId: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
@@ -151,6 +152,7 @@ types:
|
||||
status: DatasetStatus
|
||||
input: optional<unknown>
|
||||
expectedOutput: optional<unknown>
|
||||
metadata: optional<unknown>
|
||||
sourceTraceId: optional<string>
|
||||
sourceObservationId: optional<string>
|
||||
datasetId: string
|
||||
@@ -209,6 +211,7 @@ types:
|
||||
enum:
|
||||
- API
|
||||
- REVIEW
|
||||
- EVAL
|
||||
|
||||
errors:
|
||||
Error:
|
||||
|
||||
@@ -26,6 +26,9 @@ types:
|
||||
datasetName: string
|
||||
input: optional<unknown>
|
||||
expectedOutput: optional<unknown>
|
||||
metadata: optional<unknown>
|
||||
sourceTraceId: optional<string>
|
||||
sourceObservationId: optional<string>
|
||||
id:
|
||||
type: optional<string>
|
||||
docs: Dataset items are upserted on their id
|
||||
|
||||
@@ -47,3 +47,4 @@ types:
|
||||
properties:
|
||||
name: string
|
||||
description: optional<string>
|
||||
metadata: optional<unknown>
|
||||
|
||||
@@ -29,6 +29,12 @@ service:
|
||||
type: optional<string>
|
||||
allow-multiple: true
|
||||
docs: Optional filter for metrics where traces include all of these tags
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include traces on or after a certain timestamp
|
||||
toTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include traces before a certain timestamp
|
||||
response: DailyMetrics
|
||||
types:
|
||||
DailyMetrics:
|
||||
@@ -41,12 +47,13 @@ types:
|
||||
properties:
|
||||
date: date
|
||||
countTraces: integer
|
||||
countObservations: integer
|
||||
totalCost: double
|
||||
usage: list<UsageByModel>
|
||||
UsageByModel:
|
||||
docs: Daily usage of a given model. Usage corresponds to the unit set for the specific model (e.g. tokens).
|
||||
properties:
|
||||
model: string
|
||||
model: optional<string>
|
||||
inputUsage: integer
|
||||
outputUsage: integer
|
||||
totalUsage: integer
|
||||
|
||||
@@ -24,10 +24,10 @@ service:
|
||||
query-parameters:
|
||||
page:
|
||||
type: optional<integer>
|
||||
docs: page number, starts at 1
|
||||
docs: Page number, starts at 1.
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: limit of items per page
|
||||
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
|
||||
name: optional<string>
|
||||
userId: optional<string>
|
||||
type: optional<string>
|
||||
|
||||
@@ -21,15 +21,24 @@ service:
|
||||
query-parameters:
|
||||
page:
|
||||
type: optional<integer>
|
||||
docs: page number, starts at 1
|
||||
docs: Page number, starts at 1.
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: limit of items per page
|
||||
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only scores newer than this timestamp.
|
||||
source:
|
||||
type: commons.ScoreSource
|
||||
docs: Retrieve only scores from a specific source.
|
||||
operator:
|
||||
type: optional<string>
|
||||
docs: Retrieve only scores with <operator> value.
|
||||
value:
|
||||
type: optional<double>
|
||||
docs: Retrieve only scores with <operator> value.
|
||||
response: Scores
|
||||
get-by-id:
|
||||
docs: Get a score
|
||||
|
||||
@@ -16,7 +16,7 @@ service:
|
||||
docs: The unique langfuse identifier of a trace
|
||||
response: commons.TraceWithFullDetails
|
||||
list:
|
||||
docs: Get list of traces
|
||||
docs: Get list of traces.
|
||||
method: GET
|
||||
path: /traces
|
||||
request:
|
||||
@@ -24,10 +24,10 @@ service:
|
||||
query-parameters:
|
||||
page:
|
||||
type: optional<integer>
|
||||
docs: page number, starts at 1
|
||||
docs: Page number, starts at 1
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: limit of items per page
|
||||
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
fromTimestamp:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"organization": "finto",
|
||||
"version": "0.19.30"
|
||||
"version": "0.21.0"
|
||||
}
|
||||
@@ -65,7 +65,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"id\": \"example\"\n}",
|
||||
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"metadata\": \"UNKNOWN\",\n \"sourceTraceId\": \"example\",\n \"sourceObservationId\": \"example\",\n \"id\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -245,7 +245,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"example\",\n \"description\": \"example\"\n}",
|
||||
"raw": "{\n \"name\": \"example\",\n \"description\": \"example\",\n \"metadata\": \"UNKNOWN\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -379,7 +379,7 @@
|
||||
"request": {
|
||||
"description": "Get daily metrics of the Langfuse project",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/metrics/daily?page=&limit=&traceName=&userId=&tags=",
|
||||
"raw": "{{baseUrl}}/api/public/metrics/daily?page=&limit=&traceName=&userId=&tags=&fromTimestamp=&toTimestamp=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
@@ -414,6 +414,16 @@
|
||||
"key": "tags",
|
||||
"value": "",
|
||||
"description": "Optional filter for metrics where traces include all of these tags"
|
||||
},
|
||||
{
|
||||
"key": "fromTimestamp",
|
||||
"value": "",
|
||||
"description": "Optional filter to only include traces on or after a certain timestamp"
|
||||
},
|
||||
{
|
||||
"key": "toTimestamp",
|
||||
"value": "",
|
||||
"description": "Optional filter to only include traces before a certain timestamp"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
@@ -483,12 +493,12 @@
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": "page number, starts at 1"
|
||||
"description": "Page number, starts at 1."
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": "limit of items per page"
|
||||
"description": "Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit."
|
||||
},
|
||||
{
|
||||
"key": "name",
|
||||
@@ -684,7 +694,7 @@
|
||||
"request": {
|
||||
"description": "Get a list of scores",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/scores?page=&limit=&userId=&name=&fromTimestamp=",
|
||||
"raw": "{{baseUrl}}/api/public/scores?page=&limit=&userId=&name=&fromTimestamp=&source=&operator=&value=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
@@ -697,12 +707,12 @@
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": "page number, starts at 1"
|
||||
"description": "Page number, starts at 1."
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": "limit of items per page"
|
||||
"description": "Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit."
|
||||
},
|
||||
{
|
||||
"key": "userId",
|
||||
@@ -718,6 +728,21 @@
|
||||
"key": "fromTimestamp",
|
||||
"value": "",
|
||||
"description": "Retrieve only scores newer than this timestamp."
|
||||
},
|
||||
{
|
||||
"key": "source",
|
||||
"value": "",
|
||||
"description": "Retrieve only scores from a specific source."
|
||||
},
|
||||
{
|
||||
"key": "operator",
|
||||
"value": "",
|
||||
"description": "Retrieve only scores with <operator> value."
|
||||
},
|
||||
{
|
||||
"key": "value",
|
||||
"value": "",
|
||||
"description": "Retrieve only scores with <operator> value."
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
@@ -875,7 +900,7 @@
|
||||
"_type": "endpoint",
|
||||
"name": "List",
|
||||
"request": {
|
||||
"description": "Get list of traces",
|
||||
"description": "Get list of traces.",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&fromTimestamp=&orderBy=&tags=",
|
||||
"host": [
|
||||
@@ -890,12 +915,12 @@
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": "page number, starts at 1"
|
||||
"description": "Page number, starts at 1"
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": "limit of items per page"
|
||||
"description": "Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit."
|
||||
},
|
||||
{
|
||||
"key": "userId",
|
||||
|
||||
+3
-2
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.26.1",
|
||||
"version": "2.30.2",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=20",
|
||||
"pnpm": "<9"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.6.0",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"@vercel/style-guide": "^6.0.0",
|
||||
"eslint-config-next": "^14.2.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
"./src/server/auth": {
|
||||
"import": "./dist/src/server/auth.js",
|
||||
"require": "./dist/src/server/auth.js"
|
||||
},
|
||||
"./encryption": {
|
||||
"import": "./dist/src/encryption/index.js",
|
||||
"require": "./dist/src/encryption/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -28,6 +32,7 @@
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"db:migrate": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma migrate dev",
|
||||
"db:push": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma db push",
|
||||
"db:reset": "dotenv -e ../../.env npx -- prisma migrate reset",
|
||||
"db:deploy": "dotenv -e ../../.env npx -- prisma migrate deploy",
|
||||
"db:seed": "dotenv -e ../../.env -- npx prisma db seed",
|
||||
@@ -40,7 +45,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/anthropic": "^0.1.14",
|
||||
"@langchain/core": "^0.1.57",
|
||||
"@langchain/core": "^0.1.58",
|
||||
"@langchain/openai": "^0.0.28",
|
||||
"@prisma/client": "^5.12.1",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
@@ -58,7 +63,7 @@
|
||||
"@types/node": "^20.11.29",
|
||||
"@types/pg": "^8.11.5",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/parser": "^7.6.0",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
@@ -72,6 +77,6 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.2.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^1.3.1"
|
||||
"vitest": "^1.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ export type Dataset = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
metadata: unknown | null;
|
||||
project_id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
@@ -122,6 +123,7 @@ export type DatasetItem = {
|
||||
status: Generated<DatasetStatus>;
|
||||
input: unknown | null;
|
||||
expected_output: unknown | null;
|
||||
metadata: unknown | null;
|
||||
source_trace_id: string | null;
|
||||
source_observation_id: string | null;
|
||||
dataset_id: string;
|
||||
@@ -290,6 +292,13 @@ export type ObservationView = {
|
||||
calculated_total_cost: string | null;
|
||||
latency: number | null;
|
||||
};
|
||||
export type PosthogIntegration = {
|
||||
project_id: string;
|
||||
encrypted_posthog_api_key: string;
|
||||
posthog_host_name: string;
|
||||
last_sync_at: Timestamp | null;
|
||||
enabled: boolean;
|
||||
};
|
||||
export type Pricing = {
|
||||
id: string;
|
||||
model_name: string;
|
||||
@@ -335,6 +344,13 @@ export type Session = {
|
||||
user_id: string;
|
||||
expires: Timestamp;
|
||||
};
|
||||
export type SsoConfig = {
|
||||
domain: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
auth_provider: string;
|
||||
auth_config: unknown | null;
|
||||
};
|
||||
export type Trace = {
|
||||
id: string;
|
||||
external_id: string | null;
|
||||
@@ -413,11 +429,13 @@ export type DB = {
|
||||
models: Model;
|
||||
observations: Observation;
|
||||
observations_view: ObservationView;
|
||||
posthog_integrations: PosthogIntegration;
|
||||
pricings: Pricing;
|
||||
projects: Project;
|
||||
prompts: Prompt;
|
||||
scores: Score;
|
||||
Session: Session;
|
||||
sso_configs: SsoConfig;
|
||||
trace_sessions: TraceSession;
|
||||
traces: Trace;
|
||||
traces_view: TraceView;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "sso_configs" (
|
||||
"domain" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"auth_provider" TEXT NOT NULL,
|
||||
"auth_config" JSONB,
|
||||
|
||||
CONSTRAINT "sso_configs_pkey" PRIMARY KEY ("domain")
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "models_model_name_idx" ON "models"("model_name");
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "observations_internal_model_idx" ON "observations"("internal_model");
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "dataset_items" ADD COLUMN "metadata" JSONB;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "datasets" ADD COLUMN "metadata" JSONB;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "posthog_integrations" (
|
||||
"project_id" TEXT NOT NULL,
|
||||
"encrypted_posthog_api_key" TEXT NOT NULL,
|
||||
"posthog_host_name" TEXT NOT NULL,
|
||||
"last_sync_at" TIMESTAMP(3),
|
||||
"enabled" BOOLEAN NOT NULL,
|
||||
|
||||
CONSTRAINT "posthog_integrations_pkey" PRIMARY KEY ("project_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "posthog_integrations_project_id_idx" ON "posthog_integrations"("project_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "posthog_integrations" ADD CONSTRAINT "posthog_integrations_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -92,25 +92,26 @@ model VerificationToken {
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
name String
|
||||
cloudConfig Json? @map("cloud_config") // Langfuse Cloud, for zod schema see projectsRouter.ts
|
||||
members Membership[]
|
||||
traces Trace[]
|
||||
observations Observation[]
|
||||
apiKeys ApiKey[]
|
||||
dataset Dataset[]
|
||||
RawEvents Events[]
|
||||
invitations MembershipInvitation[]
|
||||
sessions TraceSession[]
|
||||
Prompt Prompt[]
|
||||
Model Model[]
|
||||
AuditLog AuditLog[]
|
||||
EvalTemplate EvalTemplate[]
|
||||
JobConfiguration JobConfiguration[]
|
||||
JobExecution JobExecution[]
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
name String
|
||||
cloudConfig Json? @map("cloud_config") // Langfuse Cloud, for zod schema see projectsRouter.ts
|
||||
members Membership[]
|
||||
traces Trace[]
|
||||
observations Observation[]
|
||||
apiKeys ApiKey[]
|
||||
dataset Dataset[]
|
||||
RawEvents Events[]
|
||||
invitations MembershipInvitation[]
|
||||
sessions TraceSession[]
|
||||
Prompt Prompt[]
|
||||
Model Model[]
|
||||
AuditLog AuditLog[]
|
||||
EvalTemplate EvalTemplate[]
|
||||
JobConfiguration JobConfiguration[]
|
||||
JobExecution JobExecution[]
|
||||
PosthogIntegration PosthogIntegration[]
|
||||
|
||||
@@map("projects")
|
||||
}
|
||||
@@ -302,6 +303,7 @@ model Observation {
|
||||
@@index(projectId)
|
||||
@@index(parentObservationId)
|
||||
@@index(model)
|
||||
@@index(internalModel)
|
||||
@@index(promptId)
|
||||
@@index([projectId, startTime, type])
|
||||
@@map("observations")
|
||||
@@ -429,6 +431,7 @@ model Dataset {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
metadata Json?
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
@@ -446,6 +449,7 @@ model DatasetItem {
|
||||
status DatasetStatus @default(ACTIVE)
|
||||
input Json?
|
||||
expectedOutput Json? @map("expected_output")
|
||||
metadata Json?
|
||||
sourceTraceId String? @map("source_trace_id")
|
||||
sourceTrace Trace? @relation(fields: [sourceTraceId], references: [id], onDelete: SetNull)
|
||||
sourceObservationId String? @map("source_observation_id")
|
||||
@@ -566,6 +570,7 @@ model Model {
|
||||
@@unique([projectId, modelName, startDate, unit])
|
||||
@@index([projectId, modelName, startDate, unit])
|
||||
@@index([projectId, modelName])
|
||||
@@index(modelName)
|
||||
@@map("models")
|
||||
}
|
||||
|
||||
@@ -680,3 +685,30 @@ model JobExecution {
|
||||
@@index([projectId])
|
||||
@@map("job_executions")
|
||||
}
|
||||
|
||||
// Single Sign-On configuration for a domain
|
||||
// This feature is part of the Enterprise Edition
|
||||
model SsoConfig {
|
||||
domain String @id @default(cuid()) // e.g. "google.com"
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
authProvider String @map("auth_provider") // e.g. "okta", ee/sso/types.ts
|
||||
|
||||
authConfig Json? @map("auth_config")
|
||||
// e.g. { "clientId": "1234", "clientSecret": "5678" }, null if credentials from env should be used
|
||||
// secrets like clientSecret are encrypted on the application level
|
||||
|
||||
@@map("sso_configs")
|
||||
}
|
||||
|
||||
model PosthogIntegration {
|
||||
projectId String @id @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
encryptedPosthogApiKey String @map("encrypted_posthog_api_key")
|
||||
posthogHostName String @map("posthog_host_name")
|
||||
lastSyncAt DateTime? @map("last_sync_at")
|
||||
enabled Boolean
|
||||
|
||||
@@index([projectId])
|
||||
@@map("posthog_integrations")
|
||||
}
|
||||
|
||||
@@ -240,6 +240,7 @@ async function main() {
|
||||
description:
|
||||
datasetNumber === 0 ? "Dataset test description" : undefined,
|
||||
projectId: project2.id,
|
||||
metadata: datasetNumber === 0 ? { key: "value" } : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -268,6 +269,7 @@ async function main() {
|
||||
Math.random() > 0.3
|
||||
? "Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both."
|
||||
: undefined,
|
||||
metadata: Math.random() > 0.5 ? { key: "value" } : undefined,
|
||||
},
|
||||
});
|
||||
datasetItemIds.push(datasetItem.id);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import crypto from "crypto";
|
||||
import { env } from "../env";
|
||||
|
||||
const ENCRYPTION_KEY: string = env.ENCRYPTION_KEY || ""; // Must be 256 bits (32 bytes, 64 hex characters)
|
||||
const IV_LENGTH: number = 16; // For AES, this is always 16
|
||||
|
||||
// Alternatively: openssl rand -hex 32
|
||||
export function keyGen() {
|
||||
return crypto.randomBytes(32).toString("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the given plain text using AES-256-GCM algorithm.
|
||||
*
|
||||
* @param {string} plainText - The text to encrypt.
|
||||
* @param {string} [keyHex=ENCRYPTION_KEY] - The encryption key in hexadecimal format.
|
||||
* @returns {string} The encrypted data in hex format, including IV and authentication tag.
|
||||
*/
|
||||
export function encrypt(
|
||||
plainText: string,
|
||||
keyHex: string = ENCRYPTION_KEY
|
||||
): string {
|
||||
const iv = crypto.randomBytes(IV_LENGTH); // Directly use Buffer returned by randomBytes
|
||||
const cipher = crypto.createCipheriv(
|
||||
"aes-256-gcm",
|
||||
Buffer.from(keyHex, "hex"),
|
||||
iv
|
||||
);
|
||||
let encrypted = cipher.update(plainText, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
// Return iv, encrypted data, and authTag as hex, combined in one line
|
||||
return iv.toString("hex") + ":" + encrypted + ":" + authTag.toString("hex");
|
||||
}
|
||||
|
||||
export function decrypt(text: string, keyHex: string = ENCRYPTION_KEY): string {
|
||||
const [ivHex, encryptedHex, authTagHex] = text.split(":");
|
||||
if (!ivHex || !encryptedHex || !authTagHex) {
|
||||
throw new Error("Invalid or corrupted cipher format");
|
||||
}
|
||||
|
||||
const iv = Buffer.from(ivHex, "hex");
|
||||
const encryptedText = Buffer.from(encryptedHex, "hex");
|
||||
const authTag = Buffer.from(authTagHex, "hex");
|
||||
|
||||
const decipher = crypto.createDecipheriv(
|
||||
"aes-256-gcm",
|
||||
Buffer.from(keyHex, "hex"),
|
||||
iv
|
||||
);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
let decrypted = decipher.update(encryptedText, undefined, "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
|
||||
return decrypted.toString();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const EnvSchema = z.object({
|
||||
ENCRYPTION_KEY: z
|
||||
.string()
|
||||
.length(
|
||||
64,
|
||||
"ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32"
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const env = EnvSchema.parse(process.env);
|
||||
@@ -9,6 +9,7 @@ export * from "./filterToPrisma";
|
||||
export * from "./tracesTable";
|
||||
export * from "./server/auth";
|
||||
export * from "./observationsTable";
|
||||
export { env } from "./env";
|
||||
|
||||
// llm api
|
||||
export * from "./server/llm/types";
|
||||
|
||||
@@ -15,6 +15,7 @@ export function getDisplaySecretKey(secretKey: string) {
|
||||
}
|
||||
|
||||
export async function hashSecretKey(key: string) {
|
||||
// legacy, uses bcrypt, transformed into hashed key upon first use
|
||||
const hashedKey = await hash(key, 11);
|
||||
return hashedKey;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
HumanMessage,
|
||||
SystemMessage,
|
||||
} from "@langchain/core/messages";
|
||||
import type { BaseCallbackHandler } from "@langchain/core/callbacks/base";
|
||||
import {
|
||||
BytesOutputParser,
|
||||
StringOutputParser,
|
||||
@@ -25,6 +26,7 @@ type LLMCompletionParams = {
|
||||
messages: ChatMessage[];
|
||||
modelParams: ModelParams;
|
||||
functionCall?: LLMFunctionCall;
|
||||
callbacks?: BaseCallbackHandler[];
|
||||
};
|
||||
|
||||
type FetchLLMCompletionParams = LLMCompletionParams & {
|
||||
@@ -34,14 +36,12 @@ type FetchLLMCompletionParams = LLMCompletionParams & {
|
||||
export async function fetchLLMCompletion(
|
||||
params: LLMCompletionParams & {
|
||||
streaming: true;
|
||||
functionCall: undefined;
|
||||
}
|
||||
): Promise<IterableReadableStream<Uint8Array>>;
|
||||
|
||||
export async function fetchLLMCompletion(
|
||||
params: LLMCompletionParams & {
|
||||
streaming: false;
|
||||
functionCall: undefined;
|
||||
}
|
||||
): Promise<string>;
|
||||
|
||||
@@ -55,7 +55,7 @@ export async function fetchLLMCompletion(
|
||||
export async function fetchLLMCompletion(
|
||||
params: FetchLLMCompletionParams
|
||||
): Promise<string | IterableReadableStream<Uint8Array> | unknown> {
|
||||
const { messages, modelParams, streaming } = params;
|
||||
const { messages, modelParams, streaming, callbacks } = params;
|
||||
const finalMessages = messages.map((message) => {
|
||||
if (message.role === ChatMessageRole.User)
|
||||
return new HumanMessage(message.content);
|
||||
@@ -73,6 +73,7 @@ export async function fetchLLMCompletion(
|
||||
temperature: modelParams.temperature,
|
||||
maxTokens: modelParams.max_tokens,
|
||||
topP: modelParams.top_p,
|
||||
callbacks,
|
||||
});
|
||||
} else {
|
||||
chatModel = new ChatOpenAI({
|
||||
@@ -81,6 +82,7 @@ export async function fetchLLMCompletion(
|
||||
temperature: modelParams.temperature,
|
||||
maxTokens: modelParams.max_tokens,
|
||||
topP: modelParams.top_p,
|
||||
callbacks,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Generated
+890
-630
File diff suppressed because it is too large
Load Diff
@@ -3,3 +3,4 @@ packages:
|
||||
- "web"
|
||||
- "worker"
|
||||
- "packages/**"
|
||||
- "ee"
|
||||
|
||||
+2
-1
@@ -5,10 +5,11 @@ FROM node:20-alpine AS alpine
|
||||
RUN apk update && apk upgrade --no-cache libcrypto3 libssl3 libc6-compat
|
||||
|
||||
FROM alpine AS base
|
||||
RUN npm install pnpm turbo --global
|
||||
RUN npm install turbo --global
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@8.15.5 --activate
|
||||
|
||||
|
||||
FROM base AS pruner
|
||||
|
||||
@@ -482,6 +482,24 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
- name: fromTimestamp
|
||||
in: query
|
||||
description: >-
|
||||
Optional filter to only include traces on or after a certain
|
||||
timestamp
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
- name: toTimestamp
|
||||
in: query
|
||||
description: Optional filter to only include traces before a certain timestamp
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
@@ -572,14 +590,16 @@ paths:
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
description: page number, starts at 1
|
||||
description: Page number, starts at 1.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
nullable: true
|
||||
- name: limit
|
||||
in: query
|
||||
description: limit of items per page
|
||||
description: >-
|
||||
Limit of items per page. If you encounter api issues due to too
|
||||
large page sizes, try to reduce the limit.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
@@ -847,14 +867,16 @@ paths:
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
description: page number, starts at 1
|
||||
description: Page number, starts at 1.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
nullable: true
|
||||
- name: limit
|
||||
in: query
|
||||
description: limit of items per page
|
||||
description: >-
|
||||
Limit of items per page. If you encounter api issues due to too
|
||||
large page sizes, try to reduce the limit.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
@@ -879,6 +901,27 @@ paths:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
- name: source
|
||||
in: query
|
||||
description: Retrieve only scores from a specific source.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/components/schemas/ScoreSource'
|
||||
- name: operator
|
||||
in: query
|
||||
description: Retrieve only scores with <operator> value.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- name: value
|
||||
in: query
|
||||
description: Retrieve only scores with <operator> value.
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
@@ -1093,21 +1136,23 @@ paths:
|
||||
security: *ref_0
|
||||
/api/public/traces:
|
||||
get:
|
||||
description: Get list of traces
|
||||
description: Get list of traces.
|
||||
operationId: trace_list
|
||||
tags:
|
||||
- Trace
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
description: page number, starts at 1
|
||||
description: Page number, starts at 1
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
nullable: true
|
||||
- name: limit
|
||||
in: query
|
||||
description: limit of items per page
|
||||
description: >-
|
||||
Limit of items per page. If you encounter api issues due to too
|
||||
large page sizes, try to reduce the limit.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
@@ -1490,6 +1535,8 @@ components:
|
||||
description:
|
||||
type: string
|
||||
nullable: true
|
||||
metadata:
|
||||
nullable: true
|
||||
projectId:
|
||||
type: string
|
||||
createdAt:
|
||||
@@ -1553,6 +1600,8 @@ components:
|
||||
nullable: true
|
||||
expectedOutput:
|
||||
nullable: true
|
||||
metadata:
|
||||
nullable: true
|
||||
sourceTraceId:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -1685,6 +1734,7 @@ components:
|
||||
enum:
|
||||
- API
|
||||
- REVIEW
|
||||
- EVAL
|
||||
CreateDatasetItemRequest:
|
||||
title: CreateDatasetItemRequest
|
||||
type: object
|
||||
@@ -1695,6 +1745,14 @@ components:
|
||||
nullable: true
|
||||
expectedOutput:
|
||||
nullable: true
|
||||
metadata:
|
||||
nullable: true
|
||||
sourceTraceId:
|
||||
type: string
|
||||
nullable: true
|
||||
sourceObservationId:
|
||||
type: string
|
||||
nullable: true
|
||||
id:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -1750,6 +1808,8 @@ components:
|
||||
description:
|
||||
type: string
|
||||
nullable: true
|
||||
metadata:
|
||||
nullable: true
|
||||
required:
|
||||
- name
|
||||
HealthResponse:
|
||||
@@ -2337,6 +2397,8 @@ components:
|
||||
type: string
|
||||
countTraces:
|
||||
type: integer
|
||||
countObservations:
|
||||
type: integer
|
||||
totalCost:
|
||||
type: number
|
||||
format: double
|
||||
@@ -2347,6 +2409,7 @@ components:
|
||||
required:
|
||||
- date
|
||||
- countTraces
|
||||
- countObservations
|
||||
- totalCost
|
||||
- usage
|
||||
UsageByModel:
|
||||
@@ -2358,6 +2421,7 @@ components:
|
||||
properties:
|
||||
model:
|
||||
type: string
|
||||
nullable: true
|
||||
inputUsage:
|
||||
type: integer
|
||||
outputUsage:
|
||||
@@ -2365,7 +2429,6 @@ components:
|
||||
totalUsage:
|
||||
type: integer
|
||||
required:
|
||||
- model
|
||||
- inputUsage
|
||||
- outputUsage
|
||||
- totalUsage
|
||||
|
||||
+14
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.26.1",
|
||||
"version": "2.30.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -26,13 +26,14 @@
|
||||
"@aws-sdk/lib-storage": "^3.550.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.554.0",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@heroicons/react": "^2.1.3",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@langchain/anthropic": "^0.1.14",
|
||||
"@langchain/core": "^0.1.57",
|
||||
"@langchain/core": "^0.1.58",
|
||||
"@langchain/openai": "^0.0.28",
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"@langfuse/ee": "workspace:*",
|
||||
"@marsidev/react-turnstile": "^0.5.4",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
@@ -59,10 +60,10 @@
|
||||
"@react-email/render": "^0.0.12",
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@sentry/nextjs": "^7.110.0",
|
||||
"@sentry/node": "^7.110.0",
|
||||
"@sentry/profiling-node": "^7.110.0",
|
||||
"@sentry/types": "^7.110.0",
|
||||
"@sentry/nextjs": "^7.110.1",
|
||||
"@sentry/node": "^7.110.1",
|
||||
"@sentry/profiling-node": "^7.110.1",
|
||||
"@sentry/types": "^7.110.1",
|
||||
"@t3-oss/env-nextjs": "^0.8.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
@@ -74,7 +75,7 @@
|
||||
"@trpc/server": "^10.45.0",
|
||||
"@uiw/codemirror-theme-github": "^4.21.25",
|
||||
"@uiw/react-codemirror": "^4.21.25",
|
||||
"ai": "^3.0.22",
|
||||
"ai": "^3.0.23",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
@@ -97,11 +98,11 @@
|
||||
"posthog-node": "^3.6.3",
|
||||
"prisma": "^5.12.1",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.51.3",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react18-json-view": "^0.2.8-canary.6",
|
||||
"sonner": "^1.4.41",
|
||||
"superjson": "2.2.1",
|
||||
@@ -124,11 +125,11 @@
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/react": "^18.2.78",
|
||||
"@types/react": "^18.2.79",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^7.6.0",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.56.0",
|
||||
@@ -138,7 +139,7 @@
|
||||
"node-mocks-http": "^1.14.1",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.5.13",
|
||||
"prettier-plugin-tailwindcss": "^0.5.14",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
|
||||
@@ -10,7 +10,7 @@ test("should redirect to home if signed in", async ({ page }) => {
|
||||
await page.goto("/auth/sign-in");
|
||||
await page.fill('input[name="email"]', "demo@langfuse.com");
|
||||
await page.fill('input[type="password"]', "password");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.click('button[data-testid="submit-email-password-sign-in-form"]');
|
||||
|
||||
// wait 2 seconds
|
||||
await page.waitForTimeout(2000);
|
||||
@@ -32,7 +32,7 @@ test("Successfully sign up & able to go to homepage", async ({ page }) => {
|
||||
await page.fill('input[name="name"]', "demo lang");
|
||||
await page.fill('input[name="email"]', randomEmailAddress());
|
||||
await page.fill('input[type="password"]', "password2");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.click('button[data-testid="submit-email-password-sign-up-form"]');
|
||||
await page.waitForTimeout(2000);
|
||||
// see get started page
|
||||
await expect(page).toHaveURL("/?getStarted=1");
|
||||
@@ -45,7 +45,7 @@ test("Successfully sign up & able to go to homepage with uppercase email", async
|
||||
await page.fill('input[name="name"]', "demo lang");
|
||||
await page.fill('input[name="email"]', "A" + randomEmailAddress());
|
||||
await page.fill('input[type="password"]', "password3");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.click('button[data-testid="submit-email-password-sign-up-form"]');
|
||||
await page.waitForTimeout(2000);
|
||||
// see get started page
|
||||
await expect(page).toHaveURL("/?getStarted=1");
|
||||
@@ -55,13 +55,13 @@ test("Signup input validation", async ({ page }) => {
|
||||
await page.goto("auth/sign-up");
|
||||
await page.fill('input[name="email"]', "notanemail");
|
||||
await page.fill('input[type="password"]', "shortPw");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.click('button[data-testid="submit-email-password-sign-up-form"]');
|
||||
await page.waitForTimeout(2000);
|
||||
await expect(page.getByText("Invalid email")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Password must be at least 8 characters long"),
|
||||
).toBeVisible();
|
||||
await page.click('button[type="submit"]');
|
||||
await page.click('button[data-testid="submit-email-password-sign-up-form"]');
|
||||
await page.waitForTimeout(2000);
|
||||
// don't see get started page
|
||||
await expect(page).not.toHaveURL("/?getStarted=1");
|
||||
|
||||
@@ -7,7 +7,7 @@ test("should see new projects dialog open after clicking new project btn", async
|
||||
await page.fill('input[name="name"]', "demo user");
|
||||
await page.fill('input[name="email"]', randomEmailAddress());
|
||||
await page.fill('input[type="password"]', "password");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.click('button[data-testid="submit-email-password-sign-up-form"]');
|
||||
await page.waitForTimeout(2000);
|
||||
await page.waitForTimeout(2000);
|
||||
expect(await page.innerHTML("data-testid=create-new-project-title")).toBe(
|
||||
@@ -24,7 +24,7 @@ test("Create a project with provided name", async ({ page }) => {
|
||||
await page.goto("/auth/sign-in");
|
||||
await page.fill('input[name="email"]', "demo@langfuse.com");
|
||||
await page.fill('input[type="password"]', "password");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.click('button[data-testid="submit-email-password-sign-in-form"]');
|
||||
await page.waitForTimeout(2000);
|
||||
await page.isVisible('[data-testid="new-project-form"]');
|
||||
await page.click('[data-testid="create-project-btn"]');
|
||||
|
||||
@@ -5,63 +5,12 @@ import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
describe("/api/public/datasets and /api/public/dataset-items API Endpoints", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create and get a dataset", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name",
|
||||
description: "dataset-description",
|
||||
});
|
||||
|
||||
const dbDataset = await prisma.dataset.findMany({
|
||||
where: {
|
||||
name: "dataset-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbDataset.length).toBeGreaterThan(0);
|
||||
|
||||
const getDataset = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/datasets/dataset-name`,
|
||||
);
|
||||
|
||||
expect(getDataset.status).toBe(200);
|
||||
expect(getDataset.body).toMatchObject({
|
||||
name: "dataset-name",
|
||||
description: "dataset-description",
|
||||
});
|
||||
});
|
||||
|
||||
it("GET datasets", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name-1",
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name-2",
|
||||
});
|
||||
|
||||
const datasetItemId = v4();
|
||||
|
||||
const createItemRes = await makeAPICall<{
|
||||
datasetName: string; // field that can break if the API changes as it is not a db column
|
||||
}>("POST", "/api/public/dataset-items", {
|
||||
datasetName: "dataset-name-2",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
id: datasetItemId,
|
||||
});
|
||||
|
||||
expect(createItemRes.status).toBe(200);
|
||||
expect(createItemRes.body).toMatchObject({
|
||||
datasetName: "dataset-name-2", // not included in db table
|
||||
});
|
||||
|
||||
const traceId = v4();
|
||||
const observationId = v4();
|
||||
const traceId = v4();
|
||||
const observationId = v4();
|
||||
beforeEach(async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
// create sample trace and observation
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
@@ -97,6 +46,62 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
],
|
||||
});
|
||||
expect(response.status).toBe(207);
|
||||
});
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create and get a dataset, include special characters", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset + name",
|
||||
description: "dataset-description",
|
||||
metadata: { foo: "bar" },
|
||||
});
|
||||
|
||||
const dbDataset = await prisma.dataset.findMany({
|
||||
where: {
|
||||
name: "dataset + name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbDataset.length).toBeGreaterThan(0);
|
||||
|
||||
const getDataset = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/datasets/${encodeURIComponent("dataset + name")}`,
|
||||
);
|
||||
|
||||
expect(getDataset.status).toBe(200);
|
||||
expect(getDataset.body).toMatchObject({
|
||||
name: "dataset + name",
|
||||
description: "dataset-description",
|
||||
metadata: { foo: "bar" },
|
||||
});
|
||||
});
|
||||
|
||||
it("GET datasets", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name-1",
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name-2",
|
||||
});
|
||||
|
||||
const datasetItemId = v4();
|
||||
|
||||
const createItemRes = await makeAPICall<{
|
||||
datasetName: string; // field that can break if the API changes as it is not a db column
|
||||
}>("POST", "/api/public/dataset-items", {
|
||||
datasetName: "dataset-name-2",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
metadata: { key: "value-dataset-item" },
|
||||
id: datasetItemId,
|
||||
});
|
||||
|
||||
expect(createItemRes.status).toBe(200);
|
||||
expect(createItemRes.body).toMatchObject({
|
||||
datasetName: "dataset-name-2", // not included in db table
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/dataset-run-items", {
|
||||
datasetItemId: datasetItemId,
|
||||
@@ -136,6 +141,9 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
metadata: { key: "value-dataset-item" },
|
||||
sourceTraceId: traceId,
|
||||
sourceObservationId: observationId,
|
||||
});
|
||||
const dbDatasetItem = await prisma.datasetItem.findFirst({
|
||||
where: {
|
||||
@@ -159,6 +167,7 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
id: dbDatasetItem!.id,
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
metadata: { key: "value-dataset-item" },
|
||||
datasetName: "dataset-name", // not included in db table
|
||||
},
|
||||
],
|
||||
@@ -173,7 +182,10 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
id: dbDatasetItem!.id,
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
metadata: { key: "value-dataset-item" },
|
||||
datasetName: "dataset-name", // not included in db table
|
||||
sourceTraceId: traceId,
|
||||
sourceObservationId: observationId,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -186,6 +198,7 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
id: "dataset-item-id",
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value-dataset-item" },
|
||||
});
|
||||
expect(item1.status).toBe(200);
|
||||
expect(item1.body).toMatchObject({
|
||||
@@ -196,6 +209,7 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
id: "dataset-item-id",
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value2" },
|
||||
metadata: ["hello-world"],
|
||||
});
|
||||
expect(item2.status).toBe(200);
|
||||
expect(item2.body).toMatchObject({
|
||||
@@ -208,22 +222,23 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
});
|
||||
expect(dbDatasetItem).not.toBeNull();
|
||||
expect(dbDatasetItem?.input).toMatchObject({ key: "value2" });
|
||||
expect(dbDatasetItem?.metadata).toMatchObject(["hello-world"]);
|
||||
});
|
||||
|
||||
it("should create and get a dataset run", async () => {
|
||||
it("should create and get a dataset run, include special characters", async () => {
|
||||
const dataset = await makeAPICall<{ id: string }>(
|
||||
"POST",
|
||||
"/api/public/datasets",
|
||||
{
|
||||
name: "dataset-name",
|
||||
name: "dataset name",
|
||||
},
|
||||
);
|
||||
expect(dataset.status).toBe(200);
|
||||
expect(dataset.body).toMatchObject({
|
||||
name: "dataset-name",
|
||||
name: "dataset name",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/dataset-items", {
|
||||
datasetName: "dataset-name",
|
||||
datasetName: "dataset name",
|
||||
id: "dataset-item-id",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
@@ -272,14 +287,14 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
{
|
||||
datasetItemId: "dataset-item-id",
|
||||
observationId: observationId,
|
||||
runName: "run-only-observation",
|
||||
runName: "run + only + observation",
|
||||
runDescription: "run-description",
|
||||
metadata: { key: "value" },
|
||||
},
|
||||
);
|
||||
const dbRunObservation = await prisma.datasetRuns.findFirst({
|
||||
where: {
|
||||
name: "run-only-observation",
|
||||
name: "run + only + observation",
|
||||
},
|
||||
include: {
|
||||
datasetRunItems: true,
|
||||
@@ -298,21 +313,22 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
|
||||
const getRunAPI = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/datasets/dataset-name/runs/run-only-observation`,
|
||||
|
||||
`/api/public/datasets/${encodeURIComponent("dataset name")}/runs/${encodeURIComponent("run + only + observation")}`,
|
||||
);
|
||||
expect(getRunAPI.status).toBe(200);
|
||||
expect(getRunAPI.body).toMatchObject({
|
||||
name: "run-only-observation",
|
||||
name: "run + only + observation",
|
||||
description: "run-description",
|
||||
metadata: { key: "value" },
|
||||
datasetId: dataset.body.id,
|
||||
datasetName: "dataset-name",
|
||||
datasetName: "dataset name",
|
||||
datasetRunItems: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
datasetItemId: "dataset-item-id",
|
||||
observationId: observationId,
|
||||
traceId: traceId,
|
||||
datasetRunName: "run-only-observation",
|
||||
datasetRunName: "run + only + observation",
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -56,6 +56,49 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
expect(fetchedObservations.body.tags).toEqual([]);
|
||||
});
|
||||
|
||||
it("should fetch a prompt with special character", async () => {
|
||||
const promptId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt + name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
config: {
|
||||
temperature: 0.1,
|
||||
},
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/prompts?name=${encodeURIComponent("prompt + name")}&version=1`,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.id).toBe(promptId);
|
||||
expect(fetchedObservations.body.name).toBe("prompt + name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt");
|
||||
expect(fetchedObservations.body.type).toBe("text");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
expect(fetchedObservations.body.config).toEqual({ temperature: 0.1 });
|
||||
expect(fetchedObservations.body.tags).toEqual([]);
|
||||
});
|
||||
|
||||
it("should fetch active prompt only if no prompt version is given", async () => {
|
||||
const promptId = uuidv4();
|
||||
|
||||
|
||||
@@ -5,8 +5,13 @@ import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
describe("/api/public/scores API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
let should_prune_db = true;
|
||||
beforeEach(async () => {
|
||||
if (should_prune_db) await pruneDatabase();
|
||||
});
|
||||
afterEach(async () => {
|
||||
if (should_prune_db) await pruneDatabase();
|
||||
});
|
||||
|
||||
it("should create score for a trace", async () => {
|
||||
await pruneDatabase();
|
||||
@@ -316,4 +321,274 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
observationId: generationId,
|
||||
});
|
||||
});
|
||||
|
||||
describe("should Filter scores", () => {
|
||||
const userId = "user-name";
|
||||
const scoreName = "score-name";
|
||||
const queryUserName = `userId=${userId}&name=${scoreName}`;
|
||||
const traceId = uuidv4();
|
||||
const generationId = uuidv4();
|
||||
const scoreId_1 = uuidv4();
|
||||
const scoreId_2 = uuidv4();
|
||||
const scoreId_3 = uuidv4();
|
||||
interface GetScoresAPIResponse {
|
||||
data: [
|
||||
{
|
||||
id: string;
|
||||
name: string;
|
||||
value: number;
|
||||
},
|
||||
];
|
||||
meta: object;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
should_prune_db = false;
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
userId: userId,
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
id: generationId,
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId_1,
|
||||
observationId: generationId,
|
||||
name: scoreName,
|
||||
value: 10.5,
|
||||
traceId: traceId,
|
||||
comment: "comment",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId_2,
|
||||
observationId: generationId,
|
||||
name: scoreName,
|
||||
value: 50.5,
|
||||
traceId: traceId,
|
||||
comment: "comment",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId_3,
|
||||
observationId: generationId,
|
||||
name: scoreName,
|
||||
value: 100.8,
|
||||
traceId: traceId,
|
||||
comment: "comment",
|
||||
});
|
||||
});
|
||||
afterAll(async () => {
|
||||
await pruneDatabase();
|
||||
});
|
||||
|
||||
it("get all scores", async () => {
|
||||
const getAllScore = await makeAPICall<{
|
||||
data: [
|
||||
{
|
||||
traceId: string;
|
||||
observationId: string;
|
||||
},
|
||||
];
|
||||
meta: object;
|
||||
}>("GET", `/api/public/scores?${queryUserName}`);
|
||||
expect(getAllScore.status).toBe(200);
|
||||
expect(getAllScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 3,
|
||||
totalPages: 1,
|
||||
});
|
||||
for (const val of getAllScore.body.data) {
|
||||
expect(val).toMatchObject({
|
||||
traceId: traceId,
|
||||
observationId: generationId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("test only operator", async () => {
|
||||
const getScore = await makeAPICall<GetScoresAPIResponse>(
|
||||
"GET",
|
||||
`/api/public/scores?${queryUserName}&operator=<`,
|
||||
);
|
||||
expect(getScore.status).toBe(200);
|
||||
expect(getScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 3,
|
||||
totalPages: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("test only value", async () => {
|
||||
const getScore = await makeAPICall<GetScoresAPIResponse>(
|
||||
"GET",
|
||||
`/api/public/scores?${queryUserName}&value=0.8`,
|
||||
);
|
||||
expect(getScore.status).toBe(200);
|
||||
expect(getScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 3,
|
||||
totalPages: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("test operator <", async () => {
|
||||
const getScore = await makeAPICall<GetScoresAPIResponse>(
|
||||
"GET",
|
||||
`/api/public/scores?${queryUserName}&operator=<&value=50`,
|
||||
);
|
||||
expect(getScore.status).toBe(200);
|
||||
expect(getScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 1,
|
||||
totalPages: 1,
|
||||
});
|
||||
expect(getScore.body.data).toMatchObject([
|
||||
{
|
||||
id: scoreId_1,
|
||||
name: scoreName,
|
||||
value: 10.5,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("test operator >", async () => {
|
||||
const getScore = await makeAPICall<GetScoresAPIResponse>(
|
||||
"GET",
|
||||
`/api/public/scores?${queryUserName}&operator=>&value=100`,
|
||||
);
|
||||
expect(getScore.status).toBe(200);
|
||||
expect(getScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 1,
|
||||
totalPages: 1,
|
||||
});
|
||||
expect(getScore.body.data).toMatchObject([
|
||||
{
|
||||
id: scoreId_3,
|
||||
name: scoreName,
|
||||
value: 100.8,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("test operator <=", async () => {
|
||||
const getScore = await makeAPICall<GetScoresAPIResponse>(
|
||||
"GET",
|
||||
`/api/public/scores?${queryUserName}&operator=<=&value=50.5`,
|
||||
);
|
||||
expect(getScore.status).toBe(200);
|
||||
expect(getScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 2,
|
||||
totalPages: 1,
|
||||
});
|
||||
expect(getScore.body.data).toMatchObject([
|
||||
{
|
||||
id: scoreId_1,
|
||||
name: scoreName,
|
||||
value: 10.5,
|
||||
},
|
||||
{
|
||||
id: scoreId_2,
|
||||
name: scoreName,
|
||||
value: 50.5,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("test operator >=", async () => {
|
||||
const getScore = await makeAPICall<GetScoresAPIResponse>(
|
||||
"GET",
|
||||
`/api/public/scores?${queryUserName}&operator=>=&value=50.5`,
|
||||
);
|
||||
expect(getScore.status).toBe(200);
|
||||
expect(getScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 2,
|
||||
totalPages: 1,
|
||||
});
|
||||
expect(getScore.body.data).toMatchObject([
|
||||
{
|
||||
id: scoreId_2,
|
||||
name: scoreName,
|
||||
value: 50.5,
|
||||
},
|
||||
{
|
||||
id: scoreId_3,
|
||||
name: scoreName,
|
||||
value: 100.8,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("test operator !=", async () => {
|
||||
const getScore = await makeAPICall<GetScoresAPIResponse>(
|
||||
"GET",
|
||||
`/api/public/scores?${queryUserName}&operator=!=&value=50.5`,
|
||||
);
|
||||
expect(getScore.status).toBe(200);
|
||||
expect(getScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 2,
|
||||
totalPages: 1,
|
||||
});
|
||||
expect(getScore.body.data).toMatchObject([
|
||||
{
|
||||
id: scoreId_1,
|
||||
name: scoreName,
|
||||
value: 10.5,
|
||||
},
|
||||
{
|
||||
id: scoreId_3,
|
||||
name: scoreName,
|
||||
value: 100.8,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("test operator =", async () => {
|
||||
const getScore = await makeAPICall<GetScoresAPIResponse>(
|
||||
"GET",
|
||||
`/api/public/scores?${queryUserName}&operator==&value=50.5`,
|
||||
);
|
||||
expect(getScore.status).toBe(200);
|
||||
expect(getScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 1,
|
||||
totalPages: 1,
|
||||
});
|
||||
expect(getScore.body.data).toMatchObject([
|
||||
{
|
||||
id: scoreId_2,
|
||||
name: scoreName,
|
||||
value: 50.5,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("test invalid operator", async () => {
|
||||
const getScore = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/scores?${queryUserName}&operator=op&value=50.5`,
|
||||
);
|
||||
expect(getScore.status).toBe(400);
|
||||
expect(getScore.body).toMatchObject({
|
||||
message: "Invalid request data",
|
||||
});
|
||||
});
|
||||
it("test invalid value", async () => {
|
||||
const getScore = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/scores?${queryUserName}&operator=<&value=myvalue`,
|
||||
);
|
||||
expect(getScore.status).toBe(400);
|
||||
expect(getScore.body).toMatchObject({
|
||||
message: "Invalid request data",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { AlertTriangle, Check } from "lucide-react";
|
||||
|
||||
import { VERSION } from "@/src/constants";
|
||||
import { env } from "@/src/env.mjs";
|
||||
@@ -42,10 +42,16 @@ export const LangfuseLogo = ({
|
||||
"flex items-center gap-2 self-stretch rounded-md px-3 py-2 text-xs ring-1 lg:-mx-2",
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
|
||||
? "bg-blue-100 text-blue-500 ring-blue-500"
|
||||
: "bg-red-100 text-red-500 ring-red-500",
|
||||
: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV"
|
||||
? "bg-green-100 text-green-500 ring-green-500"
|
||||
: "bg-red-100 text-red-500 ring-red-500",
|
||||
)}
|
||||
>
|
||||
<AlertTriangle size={16} />
|
||||
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV" ? (
|
||||
<Check size={16} />
|
||||
) : (
|
||||
<AlertTriangle size={16} />
|
||||
)}
|
||||
<span className="whitespace-nowrap">
|
||||
{["EU", "US"].includes(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION)
|
||||
? `PROD-${env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}`
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from "react";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export const Slider = (props: {
|
||||
disabled: boolean; // whether the slider is clickable in the UI
|
||||
loading?: boolean;
|
||||
onChecked?: (checked: boolean) => void;
|
||||
isChecked?: boolean; // whether the slider is active
|
||||
}) => {
|
||||
console.log("props", props);
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={props.isChecked}
|
||||
disabled={props.loading || props.disabled}
|
||||
onChange={props.onChecked}
|
||||
className={cn(
|
||||
props.isChecked ? "bg-black" : "bg-gray-200",
|
||||
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2",
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
props.isChecked ? "translate-x-5" : "translate-x-0",
|
||||
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out",
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { captureException } from "@sentry/nextjs";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const ErrorPage = ({
|
||||
title = "Error",
|
||||
message,
|
||||
}: {
|
||||
title?: string;
|
||||
message: string;
|
||||
}) => {
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center">
|
||||
<AlertCircle className="mb-4 h-12 w-12 text-red-500" />
|
||||
<h1 className="mb-4 text-xl font-bold">{title}</h1>
|
||||
<p className="mb-8 text-center">{message}</p>
|
||||
{session.status === "unauthenticated" ? (
|
||||
<Button onClick={() => void router.push("/")}>Sign In</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ErrorPageWithSentry = ({
|
||||
title = "Error",
|
||||
message,
|
||||
}: {
|
||||
title?: string;
|
||||
message: string;
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
// Capture the error with Sentry
|
||||
if (window !== undefined)
|
||||
captureException(
|
||||
new Error(`ErrorPageWithSentry rendered: ${title}, ${message}`),
|
||||
);
|
||||
}, [title, message]); // Empty dependency array means this effect runs once on mount
|
||||
|
||||
return <ErrorPage title={title} message={message} />;
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { useSession } from "next-auth/react";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
import { type Status, StatusBadge } from "./status-badge";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
|
||||
export default function Header({
|
||||
level = "h2",
|
||||
@@ -14,8 +15,10 @@ export default function Header({
|
||||
breadcrumb?: { name: string; href?: string }[];
|
||||
status?: Status;
|
||||
help?: { description: string; href?: string };
|
||||
featureBetaURL?: string;
|
||||
actionButtons?: React.ReactNode;
|
||||
level?: "h2" | "h3";
|
||||
className?: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
@@ -40,7 +43,7 @@ export default function Header({
|
||||
[...props.breadcrumb.map((i) => i.href).filter(Boolean)].pop();
|
||||
|
||||
return (
|
||||
<div className={cn(level === "h2" ? "mb-4" : "mb-2")}>
|
||||
<div className={cn(level === "h2" ? "mb-4" : "mb-2", props.className)}>
|
||||
<div>
|
||||
{backHref ? (
|
||||
<nav className="sm:hidden" aria-label="Back">
|
||||
@@ -89,7 +92,7 @@ export default function Header({
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3 md:gap-5">
|
||||
<div className="flex min-w-0 flex-row">
|
||||
<div className="flex min-w-0 flex-row justify-center align-middle">
|
||||
{level === "h2" ? (
|
||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{props.title}
|
||||
@@ -106,10 +109,25 @@ export default function Header({
|
||||
size="sm"
|
||||
/>
|
||||
) : null}
|
||||
{props.featureBetaURL ? (
|
||||
<Link
|
||||
href={props.featureBetaURL}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="flex items-center"
|
||||
>
|
||||
<Badge
|
||||
title="Feature is currently in beta. Click to learn more."
|
||||
className="ml-2"
|
||||
>
|
||||
Beta
|
||||
</Badge>
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
{props.status && <StatusBadge type={props.status} />}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{props.actionButtons ?? null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,7 +43,11 @@ const userNavigation = [
|
||||
];
|
||||
|
||||
const pathsWithoutNavigation: string[] = ["/onboarding"];
|
||||
const unauthenticatedPaths: string[] = ["/auth/sign-in", "/auth/sign-up"];
|
||||
const unauthenticatedPaths: string[] = [
|
||||
"/auth/sign-in",
|
||||
"/auth/sign-up",
|
||||
"/auth/error",
|
||||
];
|
||||
const publishablePaths: string[] = [
|
||||
"/project/[projectId]/sessions/[sessionId]",
|
||||
"/project/[projectId]/traces/[traceId]",
|
||||
@@ -457,13 +461,15 @@ export default function Layout(props: PropsWithChildren) {
|
||||
href={
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
|
||||
? "https://langfuse.com/docs/demo"
|
||||
: "https://docs-staging.langfuse.com/docs/demo"
|
||||
: "https://docs-staging.langfuse.com/docs/demo" // staging
|
||||
}
|
||||
target="_blank"
|
||||
>
|
||||
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
|
||||
? "Use Chat ↗"
|
||||
: "Use Chat (staging) ↗"}
|
||||
{
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
|
||||
? "Use Chat ↗"
|
||||
: "Use Chat (staging) ↗" // staging
|
||||
}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PenSquareIcon,
|
||||
LibraryBig,
|
||||
TerminalIcon,
|
||||
Lightbulb,
|
||||
} from "lucide-react";
|
||||
|
||||
export type Route = {
|
||||
@@ -56,6 +57,14 @@ export const ROUTES: Route[] = [
|
||||
name: "Models",
|
||||
pathname: `/project/[projectId]/models`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Evaluation",
|
||||
icon: Lightbulb,
|
||||
cloudOnly: true,
|
||||
featureFlag: "evals",
|
||||
children: [
|
||||
{
|
||||
name: "Templates",
|
||||
pathname: `/project/[projectId]/evals/templates`,
|
||||
@@ -86,6 +95,8 @@ export const ROUTES: Route[] = [
|
||||
pathname: "/project/[projectId]/playground",
|
||||
icon: TerminalIcon,
|
||||
featureFlag: "playground",
|
||||
cloudOnly: true,
|
||||
label: "Beta",
|
||||
},
|
||||
{
|
||||
name: "Datasets",
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const NoAccessError = () => {
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center">
|
||||
<AlertCircle className="mb-4 h-12 w-12 text-red-500" />
|
||||
<h1 className="mb-4 text-xl font-bold">Error</h1>
|
||||
<p className="mb-8 text-center">
|
||||
You do not have access to this resource.
|
||||
</p>
|
||||
{session.status === "unauthenticated" ? (
|
||||
<Button onClick={() => void router.push("/")}>Sign In</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { NoAccessError } from "@/src/components/no-access";
|
||||
import { ErrorPage } from "@/src/components/error-page";
|
||||
import { PublishSessionSwitch } from "@/src/components/publish-object-switch";
|
||||
import { StarSessionToggle } from "@/src/components/star-toggle";
|
||||
import { IOPreview } from "@/src/components/trace/IOPreview";
|
||||
@@ -42,7 +42,8 @@ export const SessionPage: React.FC<{
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [session.isSuccess, session.data]);
|
||||
|
||||
if (session.error?.data?.code === "UNAUTHORIZED") return <NoAccessError />;
|
||||
if (session.error?.data?.code === "UNAUTHORIZED")
|
||||
return <ErrorPage message="You do not have access to this session." />;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col overflow-hidden xl:container">
|
||||
|
||||
@@ -487,6 +487,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
observationId={observationId}
|
||||
traceId={traceId}
|
||||
io="input"
|
||||
singleLine={rowHeight === "s"}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -505,6 +506,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
observationId={observationId}
|
||||
traceId={traceId}
|
||||
io="output"
|
||||
singleLine={rowHeight === "s"}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -518,7 +520,9 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
const values = row.getValue(
|
||||
"metadata",
|
||||
) as GenerationsTableRow["metadata"];
|
||||
return !!values ? <IOTableCell data={values} /> : null;
|
||||
return !!values ? (
|
||||
<IOTableCell data={values} singleLine={rowHeight === "s"} />
|
||||
) : null;
|
||||
},
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
@@ -673,10 +677,12 @@ const GenerationsIOCell = ({
|
||||
traceId,
|
||||
observationId,
|
||||
io,
|
||||
singleLine = false,
|
||||
}: {
|
||||
traceId: string;
|
||||
observationId: string;
|
||||
io: "input" | "output";
|
||||
singleLine: boolean;
|
||||
}) => {
|
||||
const observation = api.observations.byId.useQuery(
|
||||
{
|
||||
@@ -700,6 +706,7 @@ const GenerationsIOCell = ({
|
||||
io === "output" ? observation.data?.output : observation.data?.input
|
||||
}
|
||||
className={cn(io === "output" && "bg-green-50")}
|
||||
singleLine={singleLine}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -193,7 +193,11 @@ export default function ScoresTable({
|
||||
enableHiding: true,
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("comment") as ScoresTableRow["comment"];
|
||||
return value !== undefined && <IOTableCell data={value} />;
|
||||
return (
|
||||
value !== undefined && (
|
||||
<IOTableCell data={value} singleLine={rowHeight === "s"} />
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5,7 +5,7 @@ import { TraceTableMultiSelectAction } from "@/src/components/table/data-table-m
|
||||
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { TagTracePopver } from "@/src/features/tag/components/TagTracePopver";
|
||||
import { TagTracePopover } from "@/src/features/tag/components/TagTracePopver";
|
||||
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import { Checkbox } from "@/src/components/ui/checkbox";
|
||||
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
|
||||
@@ -469,7 +469,13 @@ export default function TracesTable({
|
||||
id: "input",
|
||||
cell: ({ row }) => {
|
||||
const traceId: string = row.getValue("id");
|
||||
return <TracesIOCell traceId={traceId} io="input" />;
|
||||
return (
|
||||
<TracesIOCell
|
||||
traceId={traceId}
|
||||
io="input"
|
||||
singleLine={rowHeight === "s"}
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
@@ -480,7 +486,13 @@ export default function TracesTable({
|
||||
id: "output",
|
||||
cell: ({ row }) => {
|
||||
const traceId: string = row.getValue("id");
|
||||
return <TracesIOCell traceId={traceId} io="output" />;
|
||||
return (
|
||||
<TracesIOCell
|
||||
traceId={traceId}
|
||||
io="output"
|
||||
singleLine={rowHeight === "s"}
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
@@ -490,7 +502,7 @@ export default function TracesTable({
|
||||
header: "Metadata",
|
||||
cell: ({ row }) => {
|
||||
const values: string = row.getValue("metadata");
|
||||
return <IOTableCell data={values} />;
|
||||
return <IOTableCell data={values} singleLine={rowHeight === "s"} />;
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
@@ -540,7 +552,7 @@ export default function TracesTable({
|
||||
const filterOptionTags = traceFilterOptions.data?.tags ?? [];
|
||||
const allTags = filterOptionTags.map((t) => t.value);
|
||||
return (
|
||||
<TagTracePopver
|
||||
<TagTracePopover
|
||||
tags={tags}
|
||||
availableTags={allTags}
|
||||
projectId={projectId}
|
||||
@@ -643,9 +655,11 @@ export default function TracesTable({
|
||||
const TracesIOCell = ({
|
||||
traceId,
|
||||
io,
|
||||
singleLine = false,
|
||||
}: {
|
||||
traceId: string;
|
||||
io: "input" | "output";
|
||||
singleLine?: boolean;
|
||||
}) => {
|
||||
const trace = api.traces.byId.useQuery(
|
||||
{ traceId: traceId },
|
||||
@@ -664,6 +678,7 @@ const TracesIOCell = ({
|
||||
isLoading={trace.isLoading}
|
||||
data={io === "output" ? trace.data?.output : trace.data?.input}
|
||||
className={cn(io === "output" && "bg-green-50")}
|
||||
singleLine={singleLine}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -134,6 +134,7 @@ export const ObservationPreview = (props: {
|
||||
projectId={props.projectId}
|
||||
input={observationWithInputAndOutput.data.input}
|
||||
output={observationWithInputAndOutput.data.output}
|
||||
metadata={preloadedObservation.metadata}
|
||||
key={preloadedObservation.id}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -70,6 +70,7 @@ export const TracePreview = ({
|
||||
projectId={trace.projectId}
|
||||
input={trace.input}
|
||||
output={trace.output}
|
||||
metadata={trace.metadata}
|
||||
key={trace.id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { StarTraceDetailsToggle } from "@/src/components/star-toggle";
|
||||
import Link from "next/link";
|
||||
import { NoAccessError } from "@/src/components/no-access";
|
||||
import { ErrorPage } from "@/src/components/error-page";
|
||||
import { TagTraceDetailsPopover } from "@/src/features/tag/components/TagTraceDetailsPopover";
|
||||
import useLocalStorage from "@/src/components/useLocalStorage";
|
||||
import { Toggle } from "@/src/components/ui/toggle";
|
||||
@@ -189,7 +189,8 @@ export function TracePage({ traceId }: { traceId: string }) {
|
||||
|
||||
const totalCost = calculateDisplayTotalCost(trace.data?.observations ?? []);
|
||||
|
||||
if (trace.error?.data?.code === "UNAUTHORIZED") return <NoAccessError />;
|
||||
if (trace.error?.data?.code === "UNAUTHORIZED")
|
||||
return <ErrorPage message="You do not have access to this trace." />;
|
||||
if (!trace.data) return <div>loading...</div>;
|
||||
return (
|
||||
<div className="flex flex-col overflow-hidden 2xl:container md:h-[calc(100vh-2rem)]">
|
||||
|
||||
@@ -110,18 +110,29 @@ export const IOTableCell = ({
|
||||
data,
|
||||
isLoading = false,
|
||||
className,
|
||||
singleLine = false,
|
||||
}: {
|
||||
data: unknown;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
singleLine?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<JsonSkeleton className="h-full w-[400px] overflow-hidden px-2 py-1" />
|
||||
) : singleLine ? (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full w-[400px] self-stretch overflow-hidden overflow-y-auto truncate rounded-sm border px-2 py-0.5",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{stringifyJsonNode(data)}
|
||||
</div>
|
||||
) : (
|
||||
<JSONView
|
||||
json={data}
|
||||
json={stringifyJsonNode(data)}
|
||||
className={cn(
|
||||
"h-full w-[400px] self-stretch overflow-y-auto rounded-sm ",
|
||||
className,
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.26.1";
|
||||
export const VERSION = "v2.30.2";
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Enterprise Edition
|
||||
|
||||
This folder includes features that are only available in the Enterprise Edition of Langfuse and on Langfuse Cloud.
|
||||
|
||||
See [LICENSE](../../../LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
@@ -97,6 +97,9 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_DEMO_PROJECT_ID: z.string().optional(),
|
||||
NEXT_PUBLIC_SIGN_UP_DISABLED: z.enum(["true", "false"]).optional(),
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY: z.string().optional(),
|
||||
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
||||
NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
|
||||
NEXT_PUBLIC_CRISP_WEBSITE_ID: z.string().optional(),
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -173,6 +176,10 @@ export const env = createEnv({
|
||||
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
||||
TURNSTILE_SECRET_KEY: process.env.TURNSTILE_SECRET_KEY,
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY,
|
||||
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
||||
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
// Other
|
||||
NEXT_PUBLIC_CRISP_WEBSITE_ID: process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID,
|
||||
},
|
||||
// Skip validation in Docker builds
|
||||
// DOCKER_BUILD is set in Dockerfile
|
||||
|
||||
@@ -15,7 +15,8 @@ export type AuditableResource =
|
||||
| "session"
|
||||
| "apiKey"
|
||||
| "evalTemplate"
|
||||
| "job";
|
||||
| "job"
|
||||
| "posthogIntegration";
|
||||
|
||||
type AuditLog = {
|
||||
resourceType: AuditableResource;
|
||||
|
||||
@@ -12,18 +12,25 @@ const regions =
|
||||
flag: "🇪🇺",
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: "US",
|
||||
hostname: "us.cloud.langfuse.com",
|
||||
flag: "🇺🇸",
|
||||
},
|
||||
{
|
||||
name: "EU",
|
||||
hostname: "cloud.langfuse.com",
|
||||
flag: "🇪🇺",
|
||||
},
|
||||
];
|
||||
: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV"
|
||||
? [
|
||||
{
|
||||
name: "DEV",
|
||||
flag: "🚧",
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: "US",
|
||||
hostname: "us.cloud.langfuse.com",
|
||||
flag: "🇺🇸",
|
||||
},
|
||||
{
|
||||
name: "EU",
|
||||
hostname: "cloud.langfuse.com",
|
||||
flag: "🇪🇺",
|
||||
},
|
||||
];
|
||||
|
||||
export function CloudRegionSwitch({
|
||||
isSignUpPage,
|
||||
@@ -66,7 +73,8 @@ export function CloudRegionSwitch({
|
||||
send_instantly: true,
|
||||
},
|
||||
);
|
||||
window.location.hostname = region.hostname;
|
||||
if ("hostname" in region)
|
||||
window.location.hostname = region.hostname;
|
||||
}}
|
||||
>
|
||||
<span className="mr-2 text-xl leading-none">{region.flag}</span>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useState } from "react";
|
||||
import { DialogTrigger } from "@radix-ui/react-dialog";
|
||||
import { DatasetForm } from "@/src/features/datasets/components/DatasetForm";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { type Prisma } from "@langfuse/shared";
|
||||
|
||||
interface BaseDatasetButtonProps {
|
||||
mode: "create" | "update" | "delete";
|
||||
@@ -33,6 +34,7 @@ interface UpdateDatasetButtonProps extends BaseDatasetButtonProps {
|
||||
datasetId: string;
|
||||
datasetName: string;
|
||||
datasetDescription?: string;
|
||||
datasetMetadata?: Prisma.JsonValue;
|
||||
icon?: boolean;
|
||||
}
|
||||
|
||||
@@ -130,6 +132,7 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
datasetId={props.datasetId}
|
||||
datasetName={props.datasetName}
|
||||
datasetDescription={props.datasetDescription}
|
||||
datasetMetadata={props.datasetMetadata}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
@@ -14,6 +14,8 @@ import { api } from "@/src/utils/api";
|
||||
import { useState } from "react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import { JsonEditor } from "@/src/components/json-editor";
|
||||
import { type Prisma } from "@langfuse/shared";
|
||||
|
||||
interface BaseDatasetFormProps {
|
||||
mode: "create" | "update" | "delete";
|
||||
@@ -36,6 +38,7 @@ interface UpdateDatasetFormProps extends BaseDatasetFormProps {
|
||||
datasetId: string;
|
||||
datasetName: string;
|
||||
datasetDescription?: string;
|
||||
datasetMetadata?: Prisma.JsonValue;
|
||||
}
|
||||
|
||||
type DatasetFormProps =
|
||||
@@ -51,6 +54,21 @@ const formSchema = z.object({
|
||||
message: "Input should not be only whitespace",
|
||||
}),
|
||||
description: z.string(),
|
||||
metadata: z.string().refine(
|
||||
(value) => {
|
||||
if (value === "") return true;
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Invalid input. Please provide a JSON object or double-quoted string.",
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const DatasetForm = (props: DatasetFormProps) => {
|
||||
@@ -63,10 +81,14 @@ export const DatasetForm = (props: DatasetFormProps) => {
|
||||
? {
|
||||
name: props.datasetName,
|
||||
description: props.datasetDescription ?? "",
|
||||
metadata: props.datasetMetadata
|
||||
? JSON.stringify(props.datasetMetadata, null, 2)
|
||||
: "",
|
||||
}
|
||||
: {
|
||||
name: "",
|
||||
description: "",
|
||||
metadata: "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -173,6 +195,24 @@ export const DatasetForm = (props: DatasetFormProps) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Metadata (optional)</FormLabel>
|
||||
<FormControl>
|
||||
<JsonEditor
|
||||
defaultValue={field.value}
|
||||
onChange={(v) => {
|
||||
field.onChange(v);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
|
||||
@@ -33,14 +33,17 @@ type RowData = {
|
||||
createdAt: string;
|
||||
input: Prisma.JsonValue;
|
||||
expectedOutput: Prisma.JsonValue;
|
||||
metadata: Prisma.JsonValue;
|
||||
};
|
||||
|
||||
export function DatasetItemsTable({
|
||||
projectId,
|
||||
datasetId,
|
||||
menuItems,
|
||||
}: {
|
||||
projectId: string;
|
||||
datasetId: string;
|
||||
menuItems?: React.ReactNode;
|
||||
}) {
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
const utils = api.useUtils();
|
||||
@@ -152,7 +155,9 @@ export function DatasetItemsTable({
|
||||
enableHiding: true,
|
||||
cell: ({ row }) => {
|
||||
const input = row.getValue("input") as RowData["input"];
|
||||
return !!input ? <IOTableCell data={input} /> : null;
|
||||
return !!input ? (
|
||||
<IOTableCell data={input} singleLine={rowHeight === "s"} />
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -165,7 +170,23 @@ export function DatasetItemsTable({
|
||||
"expectedOutput",
|
||||
) as RowData["expectedOutput"];
|
||||
return !!expectedOutput ? (
|
||||
<IOTableCell data={expectedOutput} className="bg-green-50" />
|
||||
<IOTableCell
|
||||
data={expectedOutput}
|
||||
className="bg-green-50"
|
||||
singleLine={rowHeight === "s"}
|
||||
/>
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "metadata",
|
||||
header: "Metadata",
|
||||
id: "metadata",
|
||||
enableHiding: true,
|
||||
cell: ({ row }) => {
|
||||
const metadata = row.getValue("metadata") as RowData["metadata"];
|
||||
return !!metadata ? (
|
||||
<IOTableCell data={metadata} singleLine={rowHeight === "s"} />
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
@@ -224,6 +245,7 @@ export function DatasetItemsTable({
|
||||
createdAt: item.createdAt.toLocaleString(),
|
||||
input: item.input,
|
||||
expectedOutput: item.expectedOutput,
|
||||
metadata: item.metadata,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -240,6 +262,7 @@ export function DatasetItemsTable({
|
||||
setColumnVisibility={setColumnVisibility}
|
||||
rowHeight={rowHeight}
|
||||
setRowHeight={setRowHeight}
|
||||
actionButtons={menuItems}
|
||||
/>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
|
||||
@@ -161,6 +161,7 @@ export function DatasetRunItemsTable(
|
||||
traceId={trace.traceId}
|
||||
observationId={trace.observationId}
|
||||
io="input"
|
||||
singleLine={rowHeight === "s"}
|
||||
/>
|
||||
) : null;
|
||||
},
|
||||
@@ -177,6 +178,7 @@ export function DatasetRunItemsTable(
|
||||
traceId={trace.traceId}
|
||||
observationId={trace.observationId}
|
||||
io="output"
|
||||
singleLine={rowHeight === "s"}
|
||||
/>
|
||||
) : null;
|
||||
},
|
||||
@@ -194,6 +196,7 @@ export function DatasetRunItemsTable(
|
||||
datasetId={props.datasetId}
|
||||
datasetItemId={datasetItemId}
|
||||
io="expectedOutput"
|
||||
singleLine={rowHeight === "s"}
|
||||
/>
|
||||
) : null;
|
||||
},
|
||||
@@ -271,10 +274,12 @@ const TraceObservationIOCell = ({
|
||||
traceId,
|
||||
observationId,
|
||||
io,
|
||||
singleLine = false,
|
||||
}: {
|
||||
traceId: string;
|
||||
observationId?: string;
|
||||
io: "input" | "output";
|
||||
singleLine?: boolean;
|
||||
}) => {
|
||||
// conditionally fetch the trace or observation depending on the presence of observationId
|
||||
const trace = api.traces.byId.useQuery(
|
||||
@@ -312,6 +317,7 @@ const TraceObservationIOCell = ({
|
||||
isLoading={!!!observationId ? trace.isLoading : observation.isLoading}
|
||||
data={io === "output" ? data?.output : data?.input}
|
||||
className={cn(io === "output" && "bg-green-50")}
|
||||
singleLine={singleLine}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -321,11 +327,13 @@ const DatasetItemIOCell = ({
|
||||
datasetId,
|
||||
datasetItemId,
|
||||
io,
|
||||
singleLine = false,
|
||||
}: {
|
||||
projectId: string;
|
||||
datasetId: string;
|
||||
datasetItemId: string;
|
||||
io: "expectedOutput" | "input";
|
||||
singleLine?: boolean;
|
||||
}) => {
|
||||
const datasetItem = api.datasets.itemById.useQuery(
|
||||
{
|
||||
@@ -351,6 +359,7 @@ const DatasetItemIOCell = ({
|
||||
? datasetItem.data?.expectedOutput
|
||||
: datasetItem.data?.input
|
||||
}
|
||||
singleLine={singleLine}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ type RowData = {
|
||||
export function DatasetRunsTable(props: {
|
||||
projectId: string;
|
||||
datasetId: string;
|
||||
menuItems?: React.ReactNode;
|
||||
}) {
|
||||
const [paginationState, setPaginationState] = useQueryParams({
|
||||
pageIndex: withDefault(NumberParam, 0),
|
||||
@@ -138,7 +139,9 @@ export function DatasetRunsTable(props: {
|
||||
enableHiding: true,
|
||||
cell: ({ row }) => {
|
||||
const metadata: RowData["metadata"] = row.getValue("metadata");
|
||||
return !!metadata ? <IOTableCell data={metadata} /> : null;
|
||||
return !!metadata ? (
|
||||
<IOTableCell data={metadata} singleLine={rowHeight === "s"} />
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -171,6 +174,7 @@ export function DatasetRunsTable(props: {
|
||||
setColumnVisibility={setColumnVisibility}
|
||||
rowHeight={rowHeight}
|
||||
setRowHeight={setRowHeight}
|
||||
actionButtons={props.menuItems}
|
||||
/>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
|
||||
@@ -17,6 +17,9 @@ import { MoreVertical } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
|
||||
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
|
||||
import { type Prisma } from "@langfuse/shared";
|
||||
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
|
||||
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
|
||||
|
||||
type RowData = {
|
||||
key: {
|
||||
@@ -28,11 +31,14 @@ type RowData = {
|
||||
lastRunAt?: string;
|
||||
countItems: number;
|
||||
countRuns: number;
|
||||
metadata: Prisma.JsonValue;
|
||||
};
|
||||
|
||||
export function DatasetsTable(props: { projectId: string }) {
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
|
||||
const [rowHeight, setRowHeight] = useRowHeightLocalStorage("datasets", "s");
|
||||
|
||||
const [paginationState, setPaginationState] = useQueryParams({
|
||||
pageIndex: withDefault(NumberParam, 0),
|
||||
pageSize: withDefault(NumberParam, 50),
|
||||
@@ -100,6 +106,18 @@ export function DatasetsTable(props: { projectId: string }) {
|
||||
id: "lastRunAt",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "metadata",
|
||||
header: "Metadata",
|
||||
id: "metadata",
|
||||
enableHiding: true,
|
||||
cell: ({ row }) => {
|
||||
const metadata: RowData["metadata"] = row.getValue("metadata");
|
||||
return !!metadata ? (
|
||||
<IOTableCell data={metadata} singleLine={rowHeight === "s"} />
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "actions",
|
||||
@@ -145,6 +163,7 @@ export function DatasetsTable(props: { projectId: string }) {
|
||||
lastRunAt: item.lastRunAt?.toLocaleString() ?? "",
|
||||
countItems: item.countDatasetItems,
|
||||
countRuns: item.countDatasetRuns,
|
||||
metadata: item.metadata,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -162,6 +181,8 @@ export function DatasetsTable(props: { projectId: string }) {
|
||||
actionButtons={
|
||||
<DatasetActionButton projectId={props.projectId} mode="create" />
|
||||
}
|
||||
rowHeight={rowHeight}
|
||||
setRowHeight={setRowHeight}
|
||||
/>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
@@ -189,6 +210,7 @@ export function DatasetsTable(props: { projectId: string }) {
|
||||
}}
|
||||
columnVisibility={columnVisibility}
|
||||
onColumnVisibilityChange={setColumnVisibility}
|
||||
rowHeight={rowHeight}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -47,6 +47,21 @@ const formSchema = z.object({
|
||||
"Invalid input. Please provide a JSON object or double-quoted string.",
|
||||
},
|
||||
),
|
||||
metadata: z.string().refine(
|
||||
(value) => {
|
||||
if (value === "") return true;
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Invalid input. Please provide a JSON object or double-quoted string.",
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const EditDatasetItem = ({
|
||||
@@ -75,6 +90,12 @@ export const EditDatasetItem = ({
|
||||
? JSON.stringify(datasetItem.expectedOutput, null, 2)
|
||||
: "",
|
||||
);
|
||||
form.setValue(
|
||||
"metadata",
|
||||
datasetItem?.metadata
|
||||
? JSON.stringify(datasetItem.metadata, null, 2)
|
||||
: "",
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [datasetItem]);
|
||||
|
||||
@@ -83,6 +104,7 @@ export const EditDatasetItem = ({
|
||||
defaultValues: {
|
||||
input: "",
|
||||
expectedOutput: "",
|
||||
metadata: "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -99,6 +121,7 @@ export const EditDatasetItem = ({
|
||||
datasetItemId: datasetItem.id,
|
||||
input: values.input,
|
||||
expectedOutput: values.expectedOutput,
|
||||
metadata: values.metadata,
|
||||
});
|
||||
setHasChanges(false);
|
||||
}
|
||||
@@ -154,6 +177,26 @@ export const EditDatasetItem = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Metadata</FormLabel>
|
||||
<FormControl>
|
||||
<JsonEditor
|
||||
defaultValue={field.value}
|
||||
onChange={(v) => {
|
||||
setHasChanges(true);
|
||||
field.onChange(v);
|
||||
}}
|
||||
editable={hasAccess}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
|
||||
@@ -37,10 +37,8 @@ export const NewDatasetItemButton = (props: {
|
||||
New item
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="h-[calc(100vh-5rem)] max-h-none w-[calc(100vw-5rem)] max-w-none">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create new dataset item</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogContent className="h-[calc(100vh-5rem)] max-h-none w-[calc(100vw-5rem)] max-w-none items-start">
|
||||
<DialogHeader>Create new dataset item</DialogHeader>
|
||||
<NewDatasetItemForm
|
||||
projectId={props.projectId}
|
||||
datasetId={props.datasetId}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { api } from "@/src/utils/api";
|
||||
import { useState } from "react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { JsonEditor } from "@/src/components/json-editor";
|
||||
import { type Prisma } from "@langfuse/shared/src/db";
|
||||
import { type Prisma } from "@langfuse/shared";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -56,6 +56,21 @@ const formSchema = z.object({
|
||||
"Invalid input. Please provide a JSON object or double-quoted string.",
|
||||
},
|
||||
),
|
||||
metadata: z.string().refine(
|
||||
(value) => {
|
||||
if (value === "") return true;
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Invalid input. Please provide a JSON object or double-quoted string.",
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const NewDatasetItemForm = (props: {
|
||||
@@ -64,6 +79,7 @@ export const NewDatasetItemForm = (props: {
|
||||
observationId?: string;
|
||||
input?: Prisma.JsonValue;
|
||||
output?: Prisma.JsonValue;
|
||||
metadata?: Prisma.JsonValue;
|
||||
datasetId?: string;
|
||||
className?: string;
|
||||
onFormSuccess?: () => void;
|
||||
@@ -76,6 +92,7 @@ export const NewDatasetItemForm = (props: {
|
||||
datasetId: props.datasetId ?? "",
|
||||
input: props.input ? JSON.stringify(props.input, null, 2) : "",
|
||||
expectedOutput: props.output ? JSON.stringify(props.output, null, 2) : "",
|
||||
metadata: props.metadata ? JSON.stringify(props.metadata, null, 2) : "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -141,7 +158,7 @@ export const NewDatasetItemForm = (props: {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="grid flex-1 content-stretch gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="input"
|
||||
@@ -175,10 +192,26 @@ export const NewDatasetItemForm = (props: {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metadata"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col gap-2">
|
||||
<FormLabel>Metadata</FormLabel>
|
||||
<FormControl>
|
||||
<JsonEditor
|
||||
defaultValue={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={createDatasetItemMutation.isLoading}
|
||||
className="w-full"
|
||||
className="mt-auto w-full"
|
||||
>
|
||||
Add to dataset
|
||||
</Button>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import Link from "next/link";
|
||||
import { NewDatasetItemForm } from "@/src/features/datasets/components/NewDatasetItemForm";
|
||||
import { type Prisma } from "@langfuse/shared/src/db";
|
||||
import { type Prisma } from "@langfuse/shared";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
@@ -28,6 +28,7 @@ export const NewDatasetItemFromTrace = (props: {
|
||||
observationId?: string;
|
||||
input: Prisma.JsonValue;
|
||||
output: Prisma.JsonValue;
|
||||
metadata: Prisma.JsonValue;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const session = useSession();
|
||||
@@ -111,6 +112,7 @@ export const NewDatasetItemFromTrace = (props: {
|
||||
projectId={props.projectId}
|
||||
input={props.input}
|
||||
output={props.output}
|
||||
metadata={props.metadata}
|
||||
onFormSuccess={() => setOpen(false)}
|
||||
className="h-full overflow-y-auto"
|
||||
/>
|
||||
|
||||
@@ -43,6 +43,7 @@ export const datasetRouter = createTRPCRouter({
|
||||
"datasets.id",
|
||||
"datasets.name",
|
||||
"datasets.description",
|
||||
"datasets.metadata",
|
||||
"datasets.created_at as createdAt",
|
||||
"datasets.updated_at as updatedAt",
|
||||
eb.fn.count("dataset_items.id").distinct().as("countDatasetItems"),
|
||||
@@ -295,6 +296,7 @@ export const datasetRouter = createTRPCRouter({
|
||||
datasetItemId: z.string(),
|
||||
input: z.string().optional(),
|
||||
expectedOutput: z.string().optional(),
|
||||
metadata: z.string().optional(),
|
||||
sourceTraceId: z.string().optional(),
|
||||
sourceObservationId: z.string().optional(),
|
||||
status: z.enum(["ACTIVE", "ARCHIVED"]).optional(),
|
||||
@@ -327,6 +329,12 @@ export const datasetRouter = createTRPCRouter({
|
||||
: input.expectedOutput !== undefined
|
||||
? (JSON.parse(input.expectedOutput) as Prisma.InputJsonObject)
|
||||
: undefined,
|
||||
metadata:
|
||||
input.metadata === ""
|
||||
? Prisma.DbNull
|
||||
: input.metadata !== undefined
|
||||
? (JSON.parse(input.metadata) as Prisma.InputJsonObject)
|
||||
: undefined,
|
||||
sourceTraceId: input.sourceTraceId,
|
||||
sourceObservationId: input.sourceObservationId,
|
||||
status: input.status,
|
||||
@@ -348,6 +356,7 @@ export const datasetRouter = createTRPCRouter({
|
||||
projectId: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullish(),
|
||||
metadata: z.string().nullish(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -361,6 +370,12 @@ export const datasetRouter = createTRPCRouter({
|
||||
name: input.name,
|
||||
description: input.description ?? undefined,
|
||||
projectId: input.projectId,
|
||||
metadata:
|
||||
input.metadata === ""
|
||||
? Prisma.DbNull
|
||||
: !!input.metadata
|
||||
? (JSON.parse(input.metadata) as Prisma.InputJsonObject)
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -382,6 +397,7 @@ export const datasetRouter = createTRPCRouter({
|
||||
datasetId: z.string(),
|
||||
name: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
metadata: z.string().nullish(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -398,6 +414,12 @@ export const datasetRouter = createTRPCRouter({
|
||||
data: {
|
||||
name: input.name ?? undefined,
|
||||
description: input.description,
|
||||
metadata:
|
||||
input.metadata === ""
|
||||
? Prisma.DbNull
|
||||
: !!input.metadata
|
||||
? (JSON.parse(input.metadata) as Prisma.InputJsonObject)
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
await auditLog({
|
||||
@@ -442,6 +464,7 @@ export const datasetRouter = createTRPCRouter({
|
||||
datasetId: z.string(),
|
||||
input: z.string().nullish(),
|
||||
expectedOutput: z.string().nullish(),
|
||||
metadata: z.string().nullish(),
|
||||
sourceTraceId: z.string().optional(),
|
||||
sourceObservationId: z.string().optional(),
|
||||
}),
|
||||
@@ -476,6 +499,12 @@ export const datasetRouter = createTRPCRouter({
|
||||
: !!input.expectedOutput
|
||||
? (JSON.parse(input.expectedOutput) as Prisma.InputJsonObject)
|
||||
: undefined,
|
||||
metadata:
|
||||
input.metadata === ""
|
||||
? Prisma.DbNull
|
||||
: !!input.metadata
|
||||
? (JSON.parse(input.metadata) as Prisma.InputJsonObject)
|
||||
: undefined,
|
||||
datasetId: input.datasetId,
|
||||
sourceTraceId: input.sourceTraceId,
|
||||
sourceObservationId: input.sourceObservationId,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import * as React from "react";
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { RouterOutputs, api } from "@/src/utils/api";
|
||||
import { type RouterOutputs, api } from "@/src/utils/api";
|
||||
import { useRouter } from "next/router";
|
||||
import { EvalConfigForm } from "@/src/features/evals/components/eval-config-form";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Pencil } from "lucide-react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/src/components/ui/popover";
|
||||
import { useState } from "react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
|
||||
export const EvalConfigDetail = () => {
|
||||
const router = useRouter();
|
||||
@@ -95,11 +95,16 @@ export function DeactivateConfig({
|
||||
});
|
||||
|
||||
const onClick = () => {
|
||||
if (!projectId) {
|
||||
console.error("Project ID is missing");
|
||||
return;
|
||||
}
|
||||
mutEvalConfig.mutateAsync({
|
||||
projectId,
|
||||
evalConfigId: config?.id ?? "",
|
||||
updatedStatus: "INACTIVE",
|
||||
});
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -108,11 +113,10 @@ export function DeactivateConfig({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size={"sm"}
|
||||
onClick={() => void onClick()}
|
||||
disabled={!hasAccess || config?.status !== "ACTIVE"}
|
||||
loading={isLoading}
|
||||
>
|
||||
<Pencil className="h-5 w-5" />
|
||||
<Trash2 className="h-5 w-5" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
@@ -126,14 +130,7 @@ export function DeactivateConfig({
|
||||
type="button"
|
||||
variant="destructive"
|
||||
loading={mutEvalConfig.isLoading}
|
||||
onClick={() => {
|
||||
if (!projectId) {
|
||||
console.error("Project ID is missing");
|
||||
return;
|
||||
}
|
||||
void onClick();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
Deactivate Eval Job
|
||||
</Button>
|
||||
|
||||
@@ -210,7 +210,7 @@ export const EvalConfigForm = (props: {
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<SelectValue placeholder="Select a model to run this eval template" />
|
||||
<SelectValue placeholder="Select a template to run this eval config" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
|
||||
@@ -52,7 +52,7 @@ export const DetailPageNav = (props: {
|
||||
|
||||
if (ids.length > 1)
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@@ -69,7 +69,6 @@ export const DetailPageNav = (props: {
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="mr-2"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -268,7 +268,9 @@ async function* getChatCompletionStream(
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error("Failed to fetch data: " + result.statusText);
|
||||
const errorData = await result.json();
|
||||
|
||||
throw new Error(`Completion failed: ${errorData.message}`);
|
||||
}
|
||||
|
||||
const reader = result.body?.getReader();
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { ValidatedChatCompletionBody } from "@/src/features/playground/server/validateChatCompletionBody";
|
||||
import { ServerPosthog } from "@/src/server/services/posthog";
|
||||
import type { LLMResult } from "@langchain/core/outputs";
|
||||
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
|
||||
|
||||
import type { ChatMessage, ModelParams } from "@langfuse/shared";
|
||||
|
||||
export class PosthogCallbackHandler extends BaseCallbackHandler {
|
||||
public name = "PosthogCallbackHandler";
|
||||
private messages: ChatMessage[];
|
||||
private modelParams: ModelParams;
|
||||
private posthog: ServerPosthog;
|
||||
|
||||
constructor(
|
||||
public eventPrefix: string,
|
||||
public body: ValidatedChatCompletionBody,
|
||||
private userId: string,
|
||||
) {
|
||||
super();
|
||||
this.posthog = new ServerPosthog();
|
||||
this.messages = body.messages;
|
||||
this.modelParams = body.modelParams;
|
||||
}
|
||||
|
||||
async handleLLMEnd(output: LLMResult) {
|
||||
const outputString = output.generations[0][0].text;
|
||||
const properties = this.getEventProperties(outputString);
|
||||
|
||||
this.captureEvent(properties);
|
||||
await this.posthog.flushAsync();
|
||||
}
|
||||
|
||||
private getInputLength() {
|
||||
return this.messages.reduce(
|
||||
(acc, message) => acc + message.content.length,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
private getEventProperties(output: string): ChatCompletionEventProperties {
|
||||
return {
|
||||
outputLength: output.length,
|
||||
inputLength: this.getInputLength(),
|
||||
modelProvider: this.modelParams.provider,
|
||||
modelName: this.modelParams.model,
|
||||
};
|
||||
}
|
||||
|
||||
private captureEvent(properties: ChatCompletionEventProperties) {
|
||||
this.posthog.capture({
|
||||
event: this.eventPrefix + "_chat_completion",
|
||||
distinctId: this.userId,
|
||||
properties,
|
||||
});
|
||||
}
|
||||
|
||||
public async flushAsync() {
|
||||
await this.posthog.flushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
type ChatCompletionEventProperties = {
|
||||
outputLength: number;
|
||||
inputLength: number;
|
||||
modelProvider: string;
|
||||
modelName: string;
|
||||
};
|
||||
@@ -4,19 +4,23 @@ import { type NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { 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";
|
||||
|
||||
export default async function chatCompletionHandler(req: NextRequest) {
|
||||
const token = await getToken({
|
||||
req,
|
||||
cookieName: getCookieName("next-auth.session-token"),
|
||||
secret: env.NEXTAUTH_SECRET,
|
||||
});
|
||||
|
||||
if (!token)
|
||||
if (!token || !token.sub)
|
||||
// sub is the user id
|
||||
return NextResponse.json({ message: "Unauthenticated" }, { status: 401 });
|
||||
|
||||
if (req.method !== "POST")
|
||||
@@ -30,6 +34,8 @@ export default async function chatCompletionHandler(req: NextRequest) {
|
||||
try {
|
||||
body = validateChatCompletionBody(await req.json());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: "Invalid request body",
|
||||
@@ -39,13 +45,29 @@ export default async function chatCompletionHandler(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const { messages, modelParams } = body;
|
||||
const stream = await fetchLLMCompletion({
|
||||
messages,
|
||||
modelParams,
|
||||
streaming: true,
|
||||
functionCall: undefined,
|
||||
});
|
||||
try {
|
||||
const { messages, modelParams } = body;
|
||||
const stream = await fetchLLMCompletion({
|
||||
messages,
|
||||
modelParams,
|
||||
streaming: true,
|
||||
callbacks: [new PosthogCallbackHandler("playground", body, token.sub)],
|
||||
});
|
||||
|
||||
return new StreamingTextResponse(stream);
|
||||
return new StreamingTextResponse(stream);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
if (err instanceof Error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: err.message,
|
||||
error: err,
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { auditLog } from "@/src/features/audit-logs/auditLog";
|
||||
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import { decrypt, encrypt } from "@langfuse/shared/encryption";
|
||||
import { posthogIntegrationFormSchema } from "@/src/features/posthog-integration/types";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export const posthogIntegrationRouter = createTRPCRouter({
|
||||
get: protectedProjectProcedure
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "integrations:CRUD",
|
||||
});
|
||||
try {
|
||||
const dbConfig = await ctx.prisma.posthogIntegration.findFirst({
|
||||
where: {
|
||||
projectId: input.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dbConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { encryptedPosthogApiKey, ...config } = dbConfig;
|
||||
|
||||
return {
|
||||
...config,
|
||||
posthogApiKey: decrypt(encryptedPosthogApiKey),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("posthog integration get", e);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
update: protectedProjectProcedure
|
||||
.input(posthogIntegrationFormSchema.extend({ projectId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "integrations:CRUD",
|
||||
});
|
||||
await auditLog({
|
||||
session: ctx.session,
|
||||
action: "update",
|
||||
resourceType: "posthogIntegration",
|
||||
resourceId: input.projectId,
|
||||
});
|
||||
const { posthogProjectApiKey, ...config } = input;
|
||||
|
||||
const encryptedPosthogApiKey = encrypt(posthogProjectApiKey);
|
||||
|
||||
await ctx.prisma.posthogIntegration.upsert({
|
||||
where: {
|
||||
projectId: input.projectId,
|
||||
},
|
||||
create: {
|
||||
projectId: input.projectId,
|
||||
posthogHostName: config.posthogHostname,
|
||||
encryptedPosthogApiKey,
|
||||
enabled: config.enabled,
|
||||
},
|
||||
update: {
|
||||
encryptedPosthogApiKey,
|
||||
posthogHostName: config.posthogHostname,
|
||||
enabled: config.enabled,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("posthog integration update", e);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
}
|
||||
}),
|
||||
delete: protectedProjectProcedure
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "integrations:CRUD",
|
||||
});
|
||||
await auditLog({
|
||||
session: ctx.session,
|
||||
action: "delete",
|
||||
resourceType: "posthogIntegration",
|
||||
resourceId: input.projectId,
|
||||
});
|
||||
|
||||
await ctx.prisma.posthogIntegration.delete({
|
||||
where: {
|
||||
projectId: input.projectId,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("posthog integration delete", e);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const posthogIntegrationFormSchema = z.object({
|
||||
posthogHostname: z.string().url(),
|
||||
posthogProjectApiKey: z.string().refine((v) => v.startsWith("phc_"), {
|
||||
message: "PostHog Project API Key must start with 'phc_'",
|
||||
}),
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
@@ -327,7 +327,7 @@ export const NewPromptForm: React.FC<NewPromptFormProps> = (props) => {
|
||||
!initialPrompt && form.formState.errors.name?.message,
|
||||
)} // Disable button if prompt name already exists. Check is dynamic and not part of zod schema
|
||||
>
|
||||
Create prompt
|
||||
{!initialPrompt ? "Create prompt" : "Update prompt"}
|
||||
</Button>
|
||||
</form>
|
||||
{formError && (
|
||||
|
||||
@@ -85,6 +85,11 @@ export const PromptDetail = () => {
|
||||
<div className="col-span-3">
|
||||
<Header
|
||||
title={prompt.name}
|
||||
help={{
|
||||
description:
|
||||
"You can use this prompt within your application through the Langfuse SDKs and integrations. Refer to the documentation for more information.",
|
||||
href: "https://langfuse.com/docs/prompts",
|
||||
}}
|
||||
breadcrumb={[
|
||||
{
|
||||
name: "Prompts",
|
||||
|
||||
@@ -50,7 +50,7 @@ export const CreatePromptTRPCSchema = z.union([
|
||||
export type CreatePromptTRPCType = z.infer<typeof CreatePromptTRPCSchema>;
|
||||
|
||||
export const GetPromptSchema = z.object({
|
||||
name: z.string(),
|
||||
name: z.string().transform((v) => decodeURIComponent(v)),
|
||||
version: z.coerce.number().int().nullish(),
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ const scopes = [
|
||||
"project:delete",
|
||||
"project:update",
|
||||
"project:transfer",
|
||||
"integrations:CRUD",
|
||||
|
||||
"datasets:CUD",
|
||||
|
||||
@@ -45,6 +46,7 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
|
||||
"apiKeys:read",
|
||||
"apiKeys:create",
|
||||
"apiKeys:delete",
|
||||
"integrations:CRUD",
|
||||
"objects:publish",
|
||||
"objects:bookmark",
|
||||
"objects:tag",
|
||||
@@ -70,6 +72,7 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
|
||||
"apiKeys:read",
|
||||
"apiKeys:create",
|
||||
"apiKeys:delete",
|
||||
"integrations:CRUD",
|
||||
"objects:publish",
|
||||
"objects:bookmark",
|
||||
"objects:tag",
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { Crisp } from "crisp-sdk-web";
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
const CrispChat = () => {
|
||||
useEffect(() => {
|
||||
if (process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID)
|
||||
Crisp.configure(process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID);
|
||||
if (env.NEXT_PUBLIC_CRISP_WEBSITE_ID)
|
||||
Crisp.configure(env.NEXT_PUBLIC_CRISP_WEBSITE_ID);
|
||||
});
|
||||
|
||||
return null;
|
||||
|
||||
@@ -4,7 +4,7 @@ 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";
|
||||
|
||||
type TagTracePopverProps = {
|
||||
type TagTracePopoverProps = {
|
||||
tags: string[];
|
||||
availableTags: string[];
|
||||
projectId: string;
|
||||
@@ -12,13 +12,13 @@ type TagTracePopverProps = {
|
||||
tracesFilter: RouterInput["traces"]["all"];
|
||||
};
|
||||
|
||||
export function TagTracePopver({
|
||||
export function TagTracePopover({
|
||||
tags,
|
||||
availableTags,
|
||||
projectId,
|
||||
traceId,
|
||||
tracesFilter,
|
||||
}: TagTracePopverProps) {
|
||||
}: TagTracePopoverProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:tag" });
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { VERSION } from "@/src/constants";
|
||||
import { ServerPosthog } from "@/src/server/services/posthog";
|
||||
import { Prisma, prisma } from "@langfuse/shared/src/db";
|
||||
import { PostHog } from "posthog-node";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
// Safe as it is intended to be public
|
||||
const POSTHOG_API_KEY = "phc_zkMwFajk8ehObUlMth0D7DtPItFnxETi3lmSvyQDrwB";
|
||||
|
||||
// Interval between jobs in milliseconds
|
||||
const JOB_INTERVAL_MINUTES = Prisma.raw("60");
|
||||
|
||||
@@ -147,11 +144,7 @@ async function posthogTelemetry({
|
||||
clientId: string;
|
||||
}) {
|
||||
try {
|
||||
const posthog = new PostHog(POSTHOG_API_KEY, {
|
||||
host: "https://eu.posthog.com",
|
||||
});
|
||||
if (process.env.NODE_ENV === "development") posthog.debug();
|
||||
|
||||
const posthog = new ServerPosthog();
|
||||
// Count projects
|
||||
const totalProjects = await prisma.project.count();
|
||||
|
||||
|
||||
+5
-13
@@ -29,6 +29,7 @@ import "core-js/features/array/to-sorted";
|
||||
// Other CSS
|
||||
import "react18-json-view/src/style.css";
|
||||
import { DetailPageListsProvider } from "@/src/features/navigate-detail-pages/context";
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
const setProjectInPosthog = () => {
|
||||
// project
|
||||
@@ -66,10 +67,7 @@ const MyApp: AppType<{ session: Session | null }> = ({
|
||||
|
||||
useEffect(() => {
|
||||
// PostHog (cloud.langfuse.com)
|
||||
if (
|
||||
process.env.NEXT_PUBLIC_POSTHOG_KEY &&
|
||||
process.env.NEXT_PUBLIC_POSTHOG_HOST
|
||||
) {
|
||||
if (env.NEXT_PUBLIC_POSTHOG_KEY && env.NEXT_PUBLIC_POSTHOG_HOST) {
|
||||
const handleRouteChange = () => {
|
||||
setProjectInPosthog();
|
||||
posthog.capture("$pageview");
|
||||
@@ -110,17 +108,14 @@ function UserTracking() {
|
||||
useEffect(() => {
|
||||
if (session.status === "authenticated") {
|
||||
// PostHog
|
||||
if (
|
||||
process.env.NEXT_PUBLIC_POSTHOG_KEY &&
|
||||
process.env.NEXT_PUBLIC_POSTHOG_HOST
|
||||
)
|
||||
if (env.NEXT_PUBLIC_POSTHOG_KEY && env.NEXT_PUBLIC_POSTHOG_HOST)
|
||||
posthog.identify(session.data.user?.id ?? undefined, {
|
||||
environment: process.env.NODE_ENV,
|
||||
email: session.data.user?.email ?? undefined,
|
||||
name: session.data.user?.name ?? undefined,
|
||||
featureFlags: session.data.user?.featureFlags ?? undefined,
|
||||
projects: session.data.user?.projects ?? undefined,
|
||||
LANGFUSE_CLOUD_REGION: process.env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
|
||||
LANGFUSE_CLOUD_REGION: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
|
||||
});
|
||||
const emailDomain = session.data.user?.email?.split("@")[1];
|
||||
if (emailDomain)
|
||||
@@ -149,10 +144,7 @@ function UserTracking() {
|
||||
});
|
||||
} else {
|
||||
// PostHog
|
||||
if (
|
||||
process.env.NEXT_PUBLIC_POSTHOG_KEY &&
|
||||
process.env.NEXT_PUBLIC_POSTHOG_HOST
|
||||
) {
|
||||
if (env.NEXT_PUBLIC_POSTHOG_KEY && env.NEXT_PUBLIC_POSTHOG_HOST) {
|
||||
posthog.reset();
|
||||
posthog.resetGroups();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { getAuthOptions } from "@/src/server/auth";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import NextAuth from "next-auth";
|
||||
import { authOptions } from "@/src/server/auth";
|
||||
|
||||
export default NextAuth(authOptions);
|
||||
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Do whatever you want here, before the request is passed down to `NextAuth`
|
||||
const authOptions = await getAuthOptions();
|
||||
return await NextAuth(req, res, authOptions);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* This endpoint is used to add a new SSO configuration to the database.
|
||||
*
|
||||
* This is an EE feature and will return a 404 response if EE is not available.
|
||||
*/
|
||||
|
||||
import { createNewSsoConfigHandler } from "@langfuse/ee/sso";
|
||||
|
||||
export default createNewSsoConfigHandler;
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* This API endpoint checks if a custom SSO provider is configured for a given domain.
|
||||
*
|
||||
* If no custom SSO provider is configured or EE is not available, this API will return a 404 response.
|
||||
*/
|
||||
|
||||
import { getSsoAuthProviderIdForDomain } from "@langfuse/ee/sso";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
const requestSchema = z.object({
|
||||
domain: z.string().min(1),
|
||||
});
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
if (req.method !== "POST")
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
|
||||
const validBody = requestSchema.safeParse(req.body);
|
||||
if (!validBody.success) {
|
||||
return res.status(400).json({ message: "Invalid request body" });
|
||||
}
|
||||
|
||||
const providerId = await getSsoAuthProviderIdForDomain(validBody.data.domain);
|
||||
|
||||
if (!providerId) {
|
||||
return res.status(404).json({ message: "No SSO provider configured" });
|
||||
}
|
||||
|
||||
return res.status(200).json({ providerId });
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user