Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5eed6653a | ||
|
|
78aa8fdd26 | ||
|
|
9f0fbf8081 | ||
|
|
d63ce00194 | ||
|
|
9ca761bdc6 | ||
|
|
e2ec566fe4 | ||
|
|
bebc76a502 | ||
|
|
7bfbe19a8c | ||
|
|
e625053849 | ||
|
|
dc3530f195 | ||
|
|
1cf7c81386 | ||
|
|
7e31dda960 | ||
|
|
1f64991e7e | ||
|
|
577ad574d1 | ||
|
|
879ef9c8f9 | ||
|
|
a343d5b187 | ||
|
|
ac2b9eaa56 | ||
|
|
43e9cba99a | ||
|
|
601f431f69 | ||
|
|
4fd1f86f9d | ||
|
|
8245888e3c | ||
|
|
daf4e2fe02 | ||
|
|
129693fb69 | ||
|
|
204948db44 | ||
|
|
d42ba5fc99 | ||
|
|
97a539ced3 | ||
|
|
9d8dace197 | ||
|
|
bc2dc4d89c | ||
|
|
2dd5c8a8aa | ||
|
|
d17b21f04d | ||
|
|
c5a0f44bdd | ||
|
|
011b4912f2 | ||
|
|
a4d773066f | ||
|
|
25bdb1690b | ||
|
|
f8565d7772 | ||
|
|
a912057082 | ||
|
|
e0d3270f50 | ||
|
|
99e5a0b224 | ||
|
|
bccdee5410 | ||
|
|
738cbbc8cd | ||
|
|
920c52bb06 | ||
|
|
4f134790af | ||
|
|
21b3ce3c82 | ||
|
|
0531b57e1a | ||
|
|
7b857a0dc4 | ||
|
|
6c97fe3c04 | ||
|
|
1d69cbd41b | ||
|
|
ab26692913 | ||
|
|
b791544864 | ||
|
|
b35583056b | ||
|
|
4504530e3d | ||
|
|
45eed4b5c0 | ||
|
|
9fa31ba68e | ||
|
|
ea35c25269 | ||
|
|
c427791070 | ||
|
|
25220aef55 | ||
|
|
8c02d5dc22 | ||
|
|
0b60076871 | ||
|
|
b28a83531b | ||
|
|
aa4d34ae66 | ||
|
|
cca352ad13 | ||
|
|
58c96cada7 | ||
|
|
6f93389936 | ||
|
|
b8d9586490 | ||
|
|
534f5696ad | ||
|
|
4cebe2831c | ||
|
|
45a5f3b8a2 | ||
|
|
8dea9b6a3a | ||
|
|
c5b781d3f6 | ||
|
|
07469c928d | ||
|
|
5b10110dfe | ||
|
|
0eb5d1c3c0 |
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "langfuse-development",
|
||||
"forwardPorts": [3000, 5432, 6379, 8123, 9000],
|
||||
"onCreateCommand": "npm install -g pnpm@9.5.0",
|
||||
"postCreateCommand": "curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.3/migrate.linux-amd64.tar.gz | tar xvz && git restore LICENSE README.md && chmod +x migrate && sudo mv migrate /usr/bin && cp .env.dev.example .env && npm install -g @anthropic-ai/claude-code && pnpm i"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Dev container Dockerfile
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:20-bookworm
|
||||
|
||||
# Install golang-migrate for database migrations
|
||||
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.3/migrate.linux-amd64.tar.gz | tar xvz && \
|
||||
chmod +x migrate && \
|
||||
mv migrate /usr/local/bin/migrate
|
||||
|
||||
# Install pnpm globally
|
||||
RUN npm install -g pnpm@9.5.0
|
||||
|
||||
# Install Claude Code CLI
|
||||
RUN npm install -g @anthropic-ai/claude-code
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "langfuse-development",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"forwardPorts": [3000, 5432, 6379, 8123, 9000],
|
||||
"postCreateCommand": "cp .env.dev.example .env && pnpm i"
|
||||
}
|
||||
@@ -76,6 +76,7 @@ REDIS_AUTH="bitnami"
|
||||
REDIS_CLUSTER_ENABLED="true"
|
||||
REDIS_CLUSTER_NODES="127.0.0.1:6370,127.0.0.1:6371,127.0.0.1:6372,127.0.0.1:6373,127.0.0.1:6374,127.0.0.1:6375"
|
||||
LANGFUSE_INGESTION_QUEUE_SHARD_COUNT=8
|
||||
LANGFUSE_TRACE_UPSERT_QUEUE_SHARD_COUNT=4
|
||||
|
||||
# openssl rand -hex 32 used only here
|
||||
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
@@ -57,8 +57,6 @@ yarn-error.log*
|
||||
|
||||
|
||||
# vscode
|
||||
.devcontainer
|
||||
|
||||
node_modules
|
||||
**/node_modules
|
||||
**/dist
|
||||
|
||||
@@ -83,7 +83,8 @@ Depending on the file location (sync, async)
|
||||
`web` related tests must go into the `web/src/__tests__/` folder.
|
||||
```sh
|
||||
pnpm test-sync --testPathPattern="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
|
||||
pnpm test-async --testPathPattern="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
|
||||
# For tests in the async folder:
|
||||
pnpm test -- --testPathPattern="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
|
||||
```
|
||||
|
||||
### Testing in the Worker Package
|
||||
|
||||
+3
-1
@@ -241,7 +241,7 @@ We're using Jest with in the `web` package. Therefore, if you want to provide an
|
||||
There are three types of unit tests:
|
||||
|
||||
- `test-sync`
|
||||
- `test-async`
|
||||
- `test` (for async folder tests)
|
||||
- `test-client`
|
||||
|
||||
To run a specific test, for example the test: `"should handle special characters in prompt names"` in `prompts.v2.servertest.ts`, run:
|
||||
@@ -249,6 +249,8 @@ To run a specific test, for example the test: `"should handle special characters
|
||||
```sh
|
||||
cd web # or with --filter=web
|
||||
pnpm test-sync --testPathPattern="prompts\.v2\.servertest" --testNamePattern="should handle special characters in prompt names"
|
||||
# for async folder tests:
|
||||
pnpm test -- --testPathPattern="observations-api" --testNamePattern="should fetch all observations"
|
||||
```
|
||||
|
||||
To run all tests:
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Code Review Instructions
|
||||
|
||||
## Database Migrations
|
||||
|
||||
### ClickHouse
|
||||
|
||||
- ClickHouse migrations in the `packages/shared/clickhouse/migrations/clustered` directory should include `ON CLUSTER default` and should use `Replicated` merge tree table types.
|
||||
- E.g. `ReplacingMergeTree` is likely an error while `ReplicatedReplacingMergeTree` would be correct in most cases.
|
||||
- ClickHouse migrations in the `packages/shared/clickhouse/migrations/unclustered` directory must not include `ON CLUSTER` statements and must not use `Replicated` merge tree table types.
|
||||
- Migrations in `packages/shared/clickhouse/migrations/clustered` should match their counterparts in `packages/shared/clickhouse/migrations/unclustered` aside from the restrictions listed above.
|
||||
|
||||
### Postgres
|
||||
|
||||
- Most `schema.prisma` changes should produce a change in `packages/shared/prisma/migrations`.
|
||||
@@ -26,7 +26,6 @@
|
||||
"dependencies": {
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"axios": "^1.8.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"next": "^14.2.30",
|
||||
"next-auth": "^4.24.11",
|
||||
|
||||
@@ -22,6 +22,13 @@ service:
|
||||
docs: limit of items per page
|
||||
response: PaginatedAnnotationQueues
|
||||
|
||||
createQueue:
|
||||
docs: Create an annotation queue
|
||||
method: POST
|
||||
path: /annotation-queues
|
||||
request: CreateAnnotationQueueRequest
|
||||
response: AnnotationQueue
|
||||
|
||||
getQueue:
|
||||
docs: Get an annotation queue by ID
|
||||
method: GET
|
||||
@@ -168,6 +175,12 @@ types:
|
||||
data: list<AnnotationQueueItem>
|
||||
meta: pagination.MetaResponse
|
||||
|
||||
CreateAnnotationQueueRequest:
|
||||
properties:
|
||||
name: string
|
||||
description: optional<string>
|
||||
scoreConfigIds: list<string>
|
||||
|
||||
CreateAnnotationQueueItemRequest:
|
||||
properties:
|
||||
objectId: string
|
||||
|
||||
@@ -145,6 +145,13 @@ types:
|
||||
- SPAN
|
||||
- GENERATION
|
||||
- EVENT
|
||||
- AGENT
|
||||
- TOOL
|
||||
- CHAIN
|
||||
- RETRIEVER
|
||||
- EVALUATOR
|
||||
- EMBEDDING
|
||||
- GUARDRAIL
|
||||
|
||||
IngestionUsage:
|
||||
discriminated: false
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "3.95.2",
|
||||
"version": "3.98.2",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -91,7 +91,8 @@
|
||||
"nanoid": "^3.3.8",
|
||||
"katex": "^0.16.21",
|
||||
"tar-fs": "^2.1.2",
|
||||
"rollup@^4.0.0": "^4.22.4"
|
||||
"rollup@^4.0.0": "^4.22.4",
|
||||
"@types/node-fetch": "^2.6.13"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"next-auth@4.24.11": "patches/next-auth@4.24.11.patch"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE dataset_run_items_rmt ON CLUSTER default;
|
||||
@@ -0,0 +1,36 @@
|
||||
CREATE TABLE dataset_run_items_rmt ON CLUSTER default (
|
||||
-- primary identifiers
|
||||
`id` String,
|
||||
`project_id` String,
|
||||
`dataset_run_id` String,
|
||||
`dataset_item_id` String,
|
||||
`dataset_id` String,
|
||||
`trace_id` String,
|
||||
`observation_id` Nullable(String),
|
||||
|
||||
-- error field
|
||||
`error` Nullable(String),
|
||||
|
||||
-- timestamps
|
||||
`created_at` DateTime64(3) DEFAULT now(),
|
||||
`updated_at` DateTime64(3) DEFAULT now(),
|
||||
|
||||
-- denormalized immutable dataset run fields
|
||||
`dataset_run_name` String,
|
||||
`dataset_run_description` Nullable(String),
|
||||
`dataset_run_metadata` Map(LowCardinality(String), String),
|
||||
`dataset_run_created_at` DateTime64(3),
|
||||
|
||||
-- denormalized dataset item fields (mutable, but snapshots are relevant)
|
||||
`dataset_item_input` Nullable(String) CODEC(ZSTD(3)), -- json
|
||||
`dataset_item_expected_output` Nullable(String) CODEC(ZSTD(3)), -- json
|
||||
`dataset_item_metadata` Map(LowCardinality(String), String),
|
||||
|
||||
-- clickhouse engine fields
|
||||
`event_ts` DateTime64(3),
|
||||
`is_deleted` UInt8,
|
||||
|
||||
-- For dataset item lookups
|
||||
INDEX idx_dataset_item dataset_item_id TYPE bloom_filter(0.001) GRANULARITY 1,
|
||||
) ENGINE = ReplicatedReplacingMergeTree(event_ts, is_deleted)
|
||||
ORDER BY (project_id, dataset_id, dataset_run_id, id);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE dataset_run_items_rmt;
|
||||
@@ -0,0 +1,36 @@
|
||||
CREATE TABLE dataset_run_items_rmt (
|
||||
-- primary identifiers
|
||||
`id` String,
|
||||
`project_id` String,
|
||||
`dataset_run_id` String,
|
||||
`dataset_item_id` String,
|
||||
`dataset_id` String,
|
||||
`trace_id` String,
|
||||
`observation_id` Nullable(String),
|
||||
|
||||
-- error field
|
||||
`error` Nullable(String),
|
||||
|
||||
-- timestamps
|
||||
`created_at` DateTime64(3) DEFAULT now(),
|
||||
`updated_at` DateTime64(3) DEFAULT now(),
|
||||
|
||||
-- denormalized immutable dataset run fields
|
||||
`dataset_run_name` String,
|
||||
`dataset_run_description` Nullable(String),
|
||||
`dataset_run_metadata` Map(LowCardinality(String), String),
|
||||
`dataset_run_created_at` DateTime64(3),
|
||||
|
||||
-- denormalized dataset item fields (mutable, but snapshots are relevant)
|
||||
`dataset_item_input` Nullable(String) CODEC(ZSTD(3)), -- json
|
||||
`dataset_item_expected_output` Nullable(String) CODEC(ZSTD(3)), -- json
|
||||
`dataset_item_metadata` Map(LowCardinality(String), String),
|
||||
|
||||
-- clickhouse engine fields
|
||||
`event_ts` DateTime64(3),
|
||||
`is_deleted` UInt8,
|
||||
|
||||
-- For dataset item lookups
|
||||
INDEX idx_dataset_item dataset_item_id TYPE bloom_filter(0.001) GRANULARITY 1,
|
||||
) ENGINE = ReplacingMergeTree(event_ts, is_deleted)
|
||||
ORDER BY (project_id, dataset_id, dataset_run_id, id);
|
||||
@@ -73,10 +73,9 @@
|
||||
"@prisma/client": "^6.10.1",
|
||||
"@react-email/components": "^0.1.0",
|
||||
"@react-email/render": "^1.1.2",
|
||||
"@slack/oauth": "^2.6.0",
|
||||
"@slack/web-api": "^7.0.0",
|
||||
"@slack/oauth": "^3.0.3",
|
||||
"@slack/web-api": "^7.9.3",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"axios": "^1.8.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bullmq": "^5.34.10",
|
||||
"dd-trace": "^5.36.0",
|
||||
|
||||
@@ -22,6 +22,13 @@ export const LegacyPrismaObservationType = {
|
||||
SPAN: "SPAN",
|
||||
EVENT: "EVENT",
|
||||
GENERATION: "GENERATION",
|
||||
AGENT: "AGENT",
|
||||
TOOL: "TOOL",
|
||||
CHAIN: "CHAIN",
|
||||
RETRIEVER: "RETRIEVER",
|
||||
EVALUATOR: "EVALUATOR",
|
||||
EMBEDDING: "EMBEDDING",
|
||||
GUARDRAIL: "GUARDRAIL",
|
||||
} as const;
|
||||
export type LegacyPrismaObservationType =
|
||||
(typeof LegacyPrismaObservationType)[keyof typeof LegacyPrismaObservationType];
|
||||
@@ -150,6 +157,11 @@ export const ActionExecutionStatus = {
|
||||
} as const;
|
||||
export type ActionExecutionStatus =
|
||||
(typeof ActionExecutionStatus)[keyof typeof ActionExecutionStatus];
|
||||
export const SurveyName = {
|
||||
ORG_ONBOARDING: "org_onboarding",
|
||||
USER_ONBOARDING: "user_onboarding",
|
||||
} as const;
|
||||
export type SurveyName = (typeof SurveyName)[keyof typeof SurveyName];
|
||||
export type Account = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
@@ -749,6 +761,15 @@ export type SsoConfig = {
|
||||
auth_provider: string;
|
||||
auth_config: unknown | null;
|
||||
};
|
||||
export type Survey = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
survey_name: SurveyName;
|
||||
response: unknown;
|
||||
user_id: string | null;
|
||||
user_email: string | null;
|
||||
org_id: string | null;
|
||||
};
|
||||
export type TableViewPreset = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
@@ -858,6 +879,7 @@ export type DB = {
|
||||
Session: Session;
|
||||
slack_integrations: SlackIntegration;
|
||||
sso_configs: SsoConfig;
|
||||
surveys: Survey;
|
||||
table_view_presets: TableViewPreset;
|
||||
trace_media: TraceMedia;
|
||||
trace_sessions: TraceSession;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SurveyName" AS ENUM ('org_onboarding', 'user_onboarding');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "surveys" (
|
||||
"id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"survey_name" "SurveyName" NOT NULL,
|
||||
"response" JSONB NOT NULL,
|
||||
"user_id" TEXT,
|
||||
"user_email" TEXT,
|
||||
"org_id" TEXT,
|
||||
|
||||
CONSTRAINT "surveys_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "surveys" ADD CONSTRAINT "surveys_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "surveys" ADD CONSTRAINT "surveys_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "annotation_queue_assignments_project_id_queue_id_key" RENAME TO "annotation_queue_assignments_project_id_queue_id_user_id_key";
|
||||
+1
@@ -0,0 +1 @@
|
||||
DELETE FROM background_migrations WHERE id = '8d47f91b-3e5c-4a26-9f85-c12d6e4b9a3d';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
INSERT INTO background_migrations (id, name, script, args)
|
||||
VALUES ('9f32e84c-7b1d-4f59-a803-d67ae5c9b2e8', '20250814_1001_migrate_dataset_run_items_rmt_pg_to_ch', 'migrateDatasetRunItemsFromPostgresToClickhouseRmt', '{}');
|
||||
@@ -89,6 +89,7 @@ model User {
|
||||
tableViewPresetCreated TableViewPreset[] @relation("CreatedByUser")
|
||||
tableViewPresetUpdated TableViewPreset[] @relation("UpdatedByUser")
|
||||
annotationQueueAssignment AnnotationQueueAssignment[]
|
||||
surveys Survey[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@@ -113,6 +114,7 @@ model Organization {
|
||||
projects Project[]
|
||||
MembershipInvitation MembershipInvitation[]
|
||||
ApiKey ApiKey[]
|
||||
surveys Survey[]
|
||||
|
||||
@@map("organizations")
|
||||
}
|
||||
@@ -366,7 +368,6 @@ model LegacyPrismaObservation {
|
||||
version String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
// GENERATION ONLY
|
||||
model String? // user-provided model attribute
|
||||
internalModel String? @map("internal_model") // matched model.name that is matched at ingestion time, to be deprecated
|
||||
internalModelId String? @map("internal_model_id") // matched model.id that is matched at ingestion time
|
||||
@@ -407,6 +408,13 @@ enum LegacyPrismaObservationType {
|
||||
SPAN
|
||||
EVENT
|
||||
GENERATION
|
||||
AGENT
|
||||
TOOL
|
||||
CHAIN
|
||||
RETRIEVER
|
||||
EVALUATOR
|
||||
EMBEDDING
|
||||
GUARDRAIL
|
||||
|
||||
@@map("ObservationType")
|
||||
}
|
||||
@@ -1397,3 +1405,24 @@ model PendingDeletion {
|
||||
@@index([objectId, object])
|
||||
@@map("pending_deletions")
|
||||
}
|
||||
|
||||
model Survey {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
surveyName SurveyName @map("survey_name")
|
||||
response Json
|
||||
userId String? @map("user_id")
|
||||
userEmail String? @map("user_email")
|
||||
orgId String? @map("org_id")
|
||||
org Organization? @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("surveys")
|
||||
}
|
||||
|
||||
enum SurveyName {
|
||||
ORG_ONBOARDING @map("org_onboarding")
|
||||
USER_ONBOARDING @map("user_onboarding")
|
||||
|
||||
@@map("SurveyName")
|
||||
}
|
||||
|
||||
@@ -95,3 +95,57 @@ export const REALISTIC_METADATA_EXAMPLES = [
|
||||
},
|
||||
{ file_type: "JSON", validation: "passed", schema_version: "v1.2" },
|
||||
];
|
||||
|
||||
export const REALISTIC_AGENT_NAMES = [
|
||||
"AI-Coordinator",
|
||||
"TaskManager",
|
||||
"WorkflowOrchestrator",
|
||||
"PlanningAgent",
|
||||
"MultiStepAgent",
|
||||
"SmartCoordinator",
|
||||
];
|
||||
|
||||
export const REALISTIC_TOOL_NAMES = [
|
||||
"WebSearchTool",
|
||||
"CalculatorTool",
|
||||
"WeatherForecastTool",
|
||||
"EmailSenderTool",
|
||||
];
|
||||
|
||||
export const REALISTIC_CHAIN_NAMES = [
|
||||
"DataTransformationChain",
|
||||
"ProcessingPipeline",
|
||||
"TransformationChain",
|
||||
"MultiStepProcess",
|
||||
];
|
||||
|
||||
export const REALISTIC_RETRIEVER_NAMES = [
|
||||
"DocumentRetriever",
|
||||
"SemanticSearch",
|
||||
"InformationRetriever",
|
||||
"VectorSearch",
|
||||
"MemoryRetriever",
|
||||
];
|
||||
|
||||
export const REALISTIC_EVALUATOR_NAMES = [
|
||||
"QualityEvaluator",
|
||||
"RelevanceScorer",
|
||||
"AccuracyChecker",
|
||||
"ResponseEvaluator",
|
||||
"ContentScorer",
|
||||
];
|
||||
|
||||
export const REALISTIC_EMBEDDING_NAMES = [
|
||||
"TextEmbedding",
|
||||
"DocumentEncoder",
|
||||
"SemanticEncoder",
|
||||
"VectorEmbedding",
|
||||
];
|
||||
|
||||
export const REALISTIC_GUARDRAIL_NAMES = [
|
||||
"SafetyChecker",
|
||||
"ContentModerator",
|
||||
"ToxicityFilter",
|
||||
"JailbreakDetector",
|
||||
"PolicyEnforcer",
|
||||
];
|
||||
|
||||
@@ -4,6 +4,13 @@ import {
|
||||
REALISTIC_SPAN_NAMES,
|
||||
REALISTIC_GENERATION_NAMES,
|
||||
REALISTIC_MODELS,
|
||||
REALISTIC_AGENT_NAMES,
|
||||
REALISTIC_TOOL_NAMES,
|
||||
REALISTIC_CHAIN_NAMES,
|
||||
REALISTIC_RETRIEVER_NAMES,
|
||||
REALISTIC_EVALUATOR_NAMES,
|
||||
REALISTIC_EMBEDDING_NAMES,
|
||||
REALISTIC_GUARDRAIL_NAMES,
|
||||
} from "./clickhouse-seed-constants";
|
||||
import {
|
||||
generateDatasetItemId,
|
||||
@@ -365,8 +372,56 @@ export class DataGenerator {
|
||||
const observations: ObservationRecordInsertType[] = [];
|
||||
|
||||
traces.forEach((trace, traceIndex) => {
|
||||
if (this.randomBoolean(0.1)) {
|
||||
const { observations: workflowObservations } =
|
||||
this.generateComprehensiveAIWorkflowTrace(trace.id, trace.project_id);
|
||||
observations.push(...workflowObservations);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < observationsPerTrace; i++) {
|
||||
const obsType = this.randomElement(["GENERATION", "SPAN", "EVENT"]);
|
||||
const obsType = this.randomBoolean(0.8) // More "traditional" types, are more common in app
|
||||
? this.randomElement(["GENERATION", "SPAN", "EVENT"])
|
||||
: this.randomElement([
|
||||
"AGENT",
|
||||
"TOOL",
|
||||
"CHAIN",
|
||||
"RETRIEVER",
|
||||
"EVALUATOR",
|
||||
"EMBEDDING",
|
||||
"GUARDRAIL",
|
||||
]);
|
||||
|
||||
let observationName: string;
|
||||
switch (obsType) {
|
||||
case "AGENT":
|
||||
observationName = this.randomElement(REALISTIC_AGENT_NAMES);
|
||||
break;
|
||||
case "TOOL":
|
||||
observationName = this.randomElement(REALISTIC_TOOL_NAMES);
|
||||
break;
|
||||
case "CHAIN":
|
||||
observationName = this.randomElement(REALISTIC_CHAIN_NAMES);
|
||||
break;
|
||||
case "RETRIEVER":
|
||||
observationName = this.randomElement(REALISTIC_RETRIEVER_NAMES);
|
||||
break;
|
||||
case "EVALUATOR":
|
||||
observationName = this.randomElement(REALISTIC_EVALUATOR_NAMES);
|
||||
break;
|
||||
case "EMBEDDING":
|
||||
observationName = this.randomElement(REALISTIC_EMBEDDING_NAMES);
|
||||
break;
|
||||
case "GUARDRAIL":
|
||||
observationName = this.randomElement(REALISTIC_GUARDRAIL_NAMES);
|
||||
break;
|
||||
case "GENERATION":
|
||||
observationName = this.randomElement(REALISTIC_GENERATION_NAMES);
|
||||
break;
|
||||
default:
|
||||
observationName = this.randomElement(REALISTIC_SPAN_NAMES);
|
||||
break;
|
||||
}
|
||||
|
||||
const observation: ObservationRecordInsertType = createObservation({
|
||||
id: `obs-synthetic-${traceIndex}-${i}`,
|
||||
@@ -375,28 +430,28 @@ export class DataGenerator {
|
||||
parent_observation_id:
|
||||
i > 0 ? `obs-synthetic-${traceIndex}-${i - 1}` : undefined,
|
||||
type: obsType as any,
|
||||
name:
|
||||
obsType === "GENERATION"
|
||||
? this.randomElement(REALISTIC_GENERATION_NAMES)
|
||||
: this.randomElement(REALISTIC_SPAN_NAMES),
|
||||
name: observationName,
|
||||
input:
|
||||
obsType === "GENERATION"
|
||||
obsType === "GENERATION" || obsType === "EMBEDDING"
|
||||
? this.generateObservationInput()
|
||||
: undefined,
|
||||
output:
|
||||
obsType === "GENERATION"
|
||||
obsType === "GENERATION" ||
|
||||
obsType === "RETRIEVER" ||
|
||||
obsType === "EVALUATOR" ||
|
||||
obsType === "GUARDRAIL"
|
||||
? this.generateObservationOutput()
|
||||
: undefined,
|
||||
provided_model_name:
|
||||
obsType === "GENERATION"
|
||||
obsType === "GENERATION" || obsType === "EMBEDDING"
|
||||
? this.randomElement(REALISTIC_MODELS)
|
||||
: undefined,
|
||||
model_parameters:
|
||||
obsType === "GENERATION"
|
||||
obsType === "GENERATION" || obsType === "EMBEDDING"
|
||||
? JSON.stringify({ temperature: 0.7 })
|
||||
: undefined,
|
||||
usage_details:
|
||||
obsType === "GENERATION"
|
||||
obsType === "GENERATION" || obsType === "EMBEDDING"
|
||||
? {
|
||||
input: this.randomInt(20, 200),
|
||||
output: this.randomInt(10, 100),
|
||||
@@ -404,7 +459,7 @@ export class DataGenerator {
|
||||
}
|
||||
: undefined,
|
||||
provided_usage_details:
|
||||
obsType === "GENERATION"
|
||||
obsType === "GENERATION" || obsType === "EMBEDDING"
|
||||
? {
|
||||
input: this.randomInt(20, 200),
|
||||
output: this.randomInt(10, 100),
|
||||
@@ -412,7 +467,7 @@ export class DataGenerator {
|
||||
}
|
||||
: undefined,
|
||||
cost_details:
|
||||
obsType === "GENERATION"
|
||||
obsType === "GENERATION" || obsType === "EMBEDDING"
|
||||
? {
|
||||
input: this.randomInt(1, 10) / 100000,
|
||||
output: this.randomInt(1, 20) / 100000,
|
||||
@@ -420,7 +475,7 @@ export class DataGenerator {
|
||||
}
|
||||
: undefined,
|
||||
provided_cost_details:
|
||||
obsType === "GENERATION"
|
||||
obsType === "GENERATION" || obsType === "EMBEDDING"
|
||||
? {
|
||||
input: this.randomInt(1, 10) / 100000,
|
||||
output: this.randomInt(1, 20) / 100000,
|
||||
@@ -500,6 +555,252 @@ export class DataGenerator {
|
||||
return scores;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workflow trace with all possible observation types.
|
||||
*/
|
||||
generateComprehensiveAIWorkflowTrace(
|
||||
traceId: string,
|
||||
projectId: string,
|
||||
): {
|
||||
trace: TraceRecordInsertType;
|
||||
observations: ObservationRecordInsertType[];
|
||||
} {
|
||||
// Create the main trace
|
||||
const trace = createTrace({
|
||||
id: traceId,
|
||||
project_id: projectId,
|
||||
name: "AI-Agent-Workflow",
|
||||
input:
|
||||
"Analyze and summarize the latest research papers on quantum computing",
|
||||
output:
|
||||
"Here is a comprehensive summary of quantum computing research trends with key insights and recommendations.",
|
||||
user_id: this.randomBoolean(0.3)
|
||||
? `user_${this.randomInt(1, 1000)}`
|
||||
: null,
|
||||
session_id: this.randomBoolean(0.3)
|
||||
? `session_${this.randomInt(1, 100)}`
|
||||
: undefined,
|
||||
environment: "default",
|
||||
metadata: { workflowType: "comprehensive-ai", purpose: "demonstration" },
|
||||
tags: ["ai-agent", "multi-step", "comprehensive"],
|
||||
public: true,
|
||||
bookmarked: this.randomBoolean(0.2),
|
||||
});
|
||||
|
||||
const observations: ObservationRecordInsertType[] = [];
|
||||
const baseTime = Date.now();
|
||||
|
||||
// 1. AGENT - Main coordinator
|
||||
observations.push(
|
||||
createObservation({
|
||||
id: `${traceId}-agent`,
|
||||
trace_id: trace.id,
|
||||
project_id: projectId,
|
||||
type: "AGENT",
|
||||
name: this.randomElement(REALISTIC_AGENT_NAMES),
|
||||
input: "Plan and coordinate the research analysis workflow",
|
||||
output:
|
||||
"Workflow planned: retrieve documents → create embeddings → analyze → evaluate → check safety",
|
||||
start_time: baseTime,
|
||||
end_time: baseTime + 500,
|
||||
level: "DEFAULT",
|
||||
environment: trace.environment,
|
||||
metadata: { role: "coordinator", step: "1" },
|
||||
}),
|
||||
);
|
||||
|
||||
// 2. RETRIEVER - Document retrieval
|
||||
observations.push(
|
||||
createObservation({
|
||||
id: `${traceId}-retriever`,
|
||||
trace_id: trace.id,
|
||||
project_id: projectId,
|
||||
parent_observation_id: `${traceId}-agent`,
|
||||
type: "RETRIEVER",
|
||||
name: this.randomElement(REALISTIC_RETRIEVER_NAMES),
|
||||
input: "query: quantum computing research papers 2024",
|
||||
output: "Retrieved 15 relevant research papers from arXiv and IEEE",
|
||||
start_time: baseTime + 500,
|
||||
end_time: baseTime + 2000,
|
||||
level: "DEFAULT",
|
||||
environment: trace.environment,
|
||||
metadata: { documentsFound: "15", sources: "arXiv,IEEE" },
|
||||
}),
|
||||
);
|
||||
|
||||
// 3. EMBEDDING - Create document embeddings
|
||||
observations.push(
|
||||
createObservation({
|
||||
id: `${traceId}-embedding`,
|
||||
trace_id: trace.id,
|
||||
project_id: projectId,
|
||||
parent_observation_id: `${traceId}-retriever`,
|
||||
type: "EMBEDDING",
|
||||
name: this.randomElement(REALISTIC_EMBEDDING_NAMES),
|
||||
input: "15 research paper abstracts and titles",
|
||||
output: "Generated 1536-dimensional embeddings for semantic similarity",
|
||||
start_time: baseTime + 2000,
|
||||
end_time: baseTime + 3500,
|
||||
level: "DEFAULT",
|
||||
environment: trace.environment,
|
||||
metadata: {
|
||||
embeddingModel: "text-embedding-ada-002",
|
||||
dimensions: "1536",
|
||||
},
|
||||
usage_details: {
|
||||
input: this.randomInt(2000, 4000),
|
||||
total: this.randomInt(2000, 4000),
|
||||
},
|
||||
provided_usage_details: {
|
||||
input: this.randomInt(2000, 4000),
|
||||
total: this.randomInt(2000, 4000),
|
||||
},
|
||||
cost_details: {
|
||||
input: this.randomInt(5, 15) / 100000,
|
||||
total: this.randomInt(5, 15) / 100000,
|
||||
},
|
||||
provided_cost_details: {
|
||||
input: this.randomInt(5, 15) / 100000,
|
||||
total: this.randomInt(5, 15) / 100000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// 4. CHAIN - Processing pipeline
|
||||
observations.push(
|
||||
createObservation({
|
||||
id: `${traceId}-chain`,
|
||||
trace_id: trace.id,
|
||||
project_id: projectId,
|
||||
parent_observation_id: `${traceId}-embedding`,
|
||||
type: "CHAIN",
|
||||
name: this.randomElement(REALISTIC_CHAIN_NAMES),
|
||||
input: "Research papers with embeddings",
|
||||
output:
|
||||
"Processed and analyzed 15 papers through multi-step analysis chain",
|
||||
start_time: baseTime + 3500,
|
||||
end_time: baseTime + 8000,
|
||||
level: "DEFAULT",
|
||||
environment: trace.environment,
|
||||
metadata: { steps: "4", processed: "15" },
|
||||
}),
|
||||
);
|
||||
|
||||
// 5. TOOL - External API call for additional context
|
||||
observations.push(
|
||||
createObservation({
|
||||
id: `${traceId}-tool`,
|
||||
trace_id: trace.id,
|
||||
project_id: projectId,
|
||||
parent_observation_id: `${traceId}-chain`,
|
||||
type: "TOOL",
|
||||
name: this.randomElement(REALISTIC_TOOL_NAMES),
|
||||
input: "Search for quantum computing market trends",
|
||||
output:
|
||||
"Market data: $1.2B industry, 25% YoY growth, key players identified",
|
||||
start_time: baseTime + 8000,
|
||||
end_time: baseTime + 10000,
|
||||
level: "DEFAULT",
|
||||
environment: trace.environment,
|
||||
metadata: { toolType: "api-call", endpoint: "market-research" },
|
||||
}),
|
||||
);
|
||||
|
||||
// 6. GENERATION - Final summary generation
|
||||
observations.push(
|
||||
createObservation({
|
||||
id: `${traceId}-generation`,
|
||||
trace_id: trace.id,
|
||||
project_id: projectId,
|
||||
parent_observation_id: `${traceId}-tool`,
|
||||
type: "GENERATION",
|
||||
name: this.randomElement(REALISTIC_GENERATION_NAMES),
|
||||
input:
|
||||
"Synthesize research analysis and market data into comprehensive summary",
|
||||
output:
|
||||
"Generated comprehensive 2000-word analysis of quantum computing research trends",
|
||||
provided_model_name: this.randomElement(REALISTIC_MODELS),
|
||||
model_parameters: JSON.stringify({
|
||||
temperature: 0.3,
|
||||
max_tokens: 2000,
|
||||
}),
|
||||
start_time: baseTime + 10000,
|
||||
end_time: baseTime + 15000,
|
||||
level: "DEFAULT",
|
||||
environment: trace.environment,
|
||||
usage_details: {
|
||||
input: this.randomInt(1500, 2500),
|
||||
output: this.randomInt(1800, 2200),
|
||||
total: this.randomInt(3300, 4700),
|
||||
},
|
||||
provided_usage_details: {
|
||||
input: this.randomInt(1500, 2500),
|
||||
output: this.randomInt(1800, 2200),
|
||||
total: this.randomInt(3300, 4700),
|
||||
},
|
||||
cost_details: {
|
||||
input: this.randomInt(15, 25) / 100000,
|
||||
output: this.randomInt(35, 45) / 100000,
|
||||
total: this.randomInt(50, 70) / 100000,
|
||||
},
|
||||
provided_cost_details: {
|
||||
input: this.randomInt(15, 25) / 100000,
|
||||
output: this.randomInt(35, 45) / 100000,
|
||||
total: this.randomInt(50, 70) / 100000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// 7. EVALUATOR - Quality evaluation
|
||||
observations.push(
|
||||
createObservation({
|
||||
id: `${traceId}-evaluator`,
|
||||
trace_id: trace.id,
|
||||
project_id: projectId,
|
||||
parent_observation_id: `${traceId}-generation`,
|
||||
type: "EVALUATOR",
|
||||
name: this.randomElement(REALISTIC_EVALUATOR_NAMES),
|
||||
input: "Evaluate summary quality, accuracy, and completeness",
|
||||
output: "Quality score: 8.7/10, High accuracy, Comprehensive coverage",
|
||||
start_time: baseTime + 15000,
|
||||
end_time: baseTime + 16500,
|
||||
level: "DEFAULT",
|
||||
environment: trace.environment,
|
||||
metadata: {
|
||||
qualityScore: "8.7",
|
||||
accuracy: "high",
|
||||
completeness: "comprehensive",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// 8. GUARDRAIL - Safety and compliance check
|
||||
observations.push(
|
||||
createObservation({
|
||||
id: `${traceId}-guardrail`,
|
||||
trace_id: trace.id,
|
||||
project_id: projectId,
|
||||
parent_observation_id: `${traceId}-evaluator`,
|
||||
type: "GUARDRAIL",
|
||||
name: this.randomElement(REALISTIC_GUARDRAIL_NAMES),
|
||||
input: "Check content for safety, bias, and compliance issues",
|
||||
output:
|
||||
"✓ Content approved: No safety issues, Low bias detected, Compliant",
|
||||
start_time: baseTime + 16500,
|
||||
end_time: baseTime + 17000,
|
||||
level: "DEFAULT",
|
||||
environment: trace.environment,
|
||||
metadata: {
|
||||
safetyCheck: "passed",
|
||||
biasLevel: "low",
|
||||
compliance: "approved",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return { trace, observations };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates evaluation traces for testing evaluator configurations.
|
||||
* Use for: Evaluation testing, score validation, evaluator development.
|
||||
|
||||
@@ -6,8 +6,27 @@ export const ObservationType = {
|
||||
SPAN: "SPAN",
|
||||
EVENT: "EVENT",
|
||||
GENERATION: "GENERATION",
|
||||
AGENT: "AGENT",
|
||||
TOOL: "TOOL",
|
||||
CHAIN: "CHAIN",
|
||||
RETRIEVER: "RETRIEVER",
|
||||
EVALUATOR: "EVALUATOR",
|
||||
EMBEDDING: "EMBEDDING",
|
||||
GUARDRAIL: "GUARDRAIL",
|
||||
} as const;
|
||||
export const ObservationTypeDomain = z.enum(["SPAN", "EVENT", "GENERATION"]);
|
||||
|
||||
export const ObservationTypeDomain = z.enum([
|
||||
"SPAN",
|
||||
"EVENT",
|
||||
"GENERATION",
|
||||
"AGENT",
|
||||
"TOOL",
|
||||
"CHAIN",
|
||||
"RETRIEVER",
|
||||
"EVALUATOR",
|
||||
"EMBEDDING",
|
||||
"GUARDRAIL",
|
||||
]);
|
||||
export type ObservationType = z.infer<typeof ObservationTypeDomain>;
|
||||
|
||||
export const ObservationLevel = {
|
||||
@@ -74,3 +93,29 @@ export const ObservationSchema = z.object({
|
||||
});
|
||||
|
||||
export type Observation = z.infer<typeof ObservationSchema>;
|
||||
|
||||
/**
|
||||
* Returns true if an observation type is generation-like, meaning it could include LLM calls
|
||||
* and potentially has similar input/output fields.
|
||||
*/
|
||||
export const GenerationLikeObservationTypes = [
|
||||
ObservationType.GENERATION,
|
||||
ObservationType.AGENT,
|
||||
ObservationType.TOOL,
|
||||
ObservationType.CHAIN,
|
||||
ObservationType.RETRIEVER,
|
||||
ObservationType.EVALUATOR,
|
||||
ObservationType.EMBEDDING,
|
||||
ObservationType.GUARDRAIL,
|
||||
] as const;
|
||||
|
||||
export const isGenerationLike = (observationType: ObservationType): boolean => {
|
||||
return GenerationLikeObservationTypes.includes(observationType as any);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all generation-like observation types for use in filters and queries.
|
||||
*/
|
||||
export const getGenerationLikeTypes = (): ObservationType[] => {
|
||||
return [...GenerationLikeObservationTypes];
|
||||
};
|
||||
|
||||
@@ -50,6 +50,10 @@ const EnvSchema = z.object({
|
||||
.nonnegative()
|
||||
.default(15_000),
|
||||
LANGFUSE_INGESTION_QUEUE_SHARD_COUNT: z.coerce.number().positive().default(1),
|
||||
LANGFUSE_TRACE_UPSERT_QUEUE_SHARD_COUNT: z.coerce
|
||||
.number()
|
||||
.positive()
|
||||
.default(1),
|
||||
LANGFUSE_TRACE_DELETE_DELAY_MS: z.coerce
|
||||
.number()
|
||||
.nonnegative()
|
||||
@@ -154,6 +158,12 @@ const EnvSchema = z.object({
|
||||
LANGFUSE_EXPERIMENT_INSERT_INTO_AGGREGATING_MERGE_TREES: z
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
LANGFUSE_EXPERIMENT_WHITELISTED_AMT_TABLES: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((s) =>
|
||||
s ? s.split(",").map((s) => s.toLowerCase().trim()) : [],
|
||||
),
|
||||
LANGFUSE_INGESTION_PROCESSING_SAMPLED_PROJECTS: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -186,10 +196,10 @@ const EnvSchema = z.object({
|
||||
return new Map<string, number>();
|
||||
}
|
||||
}),
|
||||
|
||||
SLACK_CLIENT_ID: z.string().optional(),
|
||||
SLACK_CLIENT_SECRET: z.string().optional(),
|
||||
SLACK_STATE_SECRET: z.string().optional(),
|
||||
HTTPS_PROXY: z.string().optional(),
|
||||
|
||||
LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT: z.coerce
|
||||
.number()
|
||||
|
||||
@@ -5,6 +5,13 @@ export const langfuseObjects = [
|
||||
"span",
|
||||
"generation",
|
||||
"event",
|
||||
"agent",
|
||||
"tool",
|
||||
"chain",
|
||||
"retriever",
|
||||
"evaluator",
|
||||
"embedding",
|
||||
"guardrail",
|
||||
"dataset_item",
|
||||
] as const;
|
||||
|
||||
@@ -80,6 +87,41 @@ export const availableTraceEvalVariables = [
|
||||
display: "Event",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "agent",
|
||||
display: "Agent",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "tool",
|
||||
display: "Tool",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "chain",
|
||||
display: "Chain",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "retriever",
|
||||
display: "Retriever",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "evaluator",
|
||||
display: "Evaluator",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "embedding",
|
||||
display: "Embedding",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "guardrail",
|
||||
display: "Guardrail",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
];
|
||||
|
||||
export const availableDatasetEvalVariables = [
|
||||
|
||||
@@ -27,16 +27,42 @@ export const parseUnknownToString = (value: unknown): string => {
|
||||
return String(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively parses JSON strings that may have been encoded multiple times.
|
||||
* This handles cases where data has been JSON.stringify'd multiple times.
|
||||
*
|
||||
* @param value - The potentially multi-encoded JSON string
|
||||
* @returns The final parsed object or the original value if parsing fails
|
||||
*/
|
||||
function parseMultiEncodedJson(value: unknown): unknown {
|
||||
if (typeof value !== "string") {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
|
||||
// If result is still a string, it might be double-encoded - recurse
|
||||
if (typeof parsed === "string") {
|
||||
return parseMultiEncodedJson(parsed);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch {
|
||||
// If parsing fails, return original value
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonDefault(selectedColumn: unknown, jsonSelector: string) {
|
||||
// selectedColumn should already be preprocessed by preprocessObjectWithJsonFields
|
||||
// so we can directly use it with JSONPath
|
||||
const result = JSONPath({
|
||||
path: jsonSelector,
|
||||
json:
|
||||
typeof selectedColumn === "string"
|
||||
? JSON.parse(selectedColumn)
|
||||
: selectedColumn,
|
||||
json: selectedColumn as any, // JSONPath accepts unknown but types are strict
|
||||
});
|
||||
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
return Array.isArray(result) && result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
export function extractValueFromObject(
|
||||
@@ -44,7 +70,13 @@ export function extractValueFromObject(
|
||||
mapping: z.infer<typeof variableMapping>,
|
||||
parseJson?: (selectedColumn: unknown, jsonSelector: string) => unknown, // eslint-disable-line no-unused-vars
|
||||
): { value: string; error: Error | null } {
|
||||
const selectedColumn = obj[mapping.selectedColumnId];
|
||||
let selectedColumn = obj[mapping.selectedColumnId];
|
||||
|
||||
// Simple preprocessing: attempt to parse to valid JSON object
|
||||
if (typeof selectedColumn === "string") {
|
||||
selectedColumn = parseMultiEncodedJson(selectedColumn);
|
||||
}
|
||||
|
||||
const jsonParser = parseJson || parseJsonDefault;
|
||||
|
||||
let jsonSelectedColumn;
|
||||
|
||||
@@ -2,7 +2,7 @@ export const ClickhouseTableNames = {
|
||||
traces: "traces",
|
||||
observations: "observations",
|
||||
scores: "scores",
|
||||
dataset_run_items: "dataset_run_items",
|
||||
dataset_run_items_rmt: "dataset_run_items_rmt",
|
||||
|
||||
// Virtual tables for dashboards
|
||||
// TODO: Check if we can do this more elegantly
|
||||
|
||||
@@ -27,6 +27,13 @@ export const getClickhouseEntityType = (
|
||||
case eventTypes.SPAN_UPDATE:
|
||||
case eventTypes.GENERATION_CREATE:
|
||||
case eventTypes.GENERATION_UPDATE:
|
||||
case eventTypes.AGENT_CREATE:
|
||||
case eventTypes.TOOL_CREATE:
|
||||
case eventTypes.CHAIN_CREATE:
|
||||
case eventTypes.RETRIEVER_CREATE:
|
||||
case eventTypes.EVALUATOR_CREATE:
|
||||
case eventTypes.EMBEDDING_CREATE:
|
||||
case eventTypes.GUARDRAIL_CREATE:
|
||||
return "observation";
|
||||
case eventTypes.SCORE_CREATE:
|
||||
return "score";
|
||||
|
||||
@@ -88,7 +88,7 @@ async function removeIngestionEventsFromS3AndDeleteClickhouseRefs(p: {
|
||||
);
|
||||
await softDeleteInClickhouse(blobStorageRefs);
|
||||
logger.info(
|
||||
`Deleted batch ${batch} of size ${blobStorageRefs.length} for ${projectId} of deleting s3 refs`,
|
||||
`Deleted last batch ${batch} of size ${blobStorageRefs.length} for ${projectId} of deleting s3 refs`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ export async function executeWithDatasetRunItemsStrategy<TInput, TOutput>({
|
||||
return await postgresExecution(input);
|
||||
}
|
||||
} else {
|
||||
// Read from PostgreSQL
|
||||
return await postgresExecution(input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export * from "./llm/fetchLLMCompletion";
|
||||
export * from "./llm/utils";
|
||||
export * from "./llm/types";
|
||||
export * from "./llm/compileChatMessages";
|
||||
export * from "./llm/testModelCall";
|
||||
export * from "./utils/DatabaseReadStream";
|
||||
export * from "./utils/transforms";
|
||||
export * from "./clickhouse/client";
|
||||
|
||||
@@ -16,6 +16,8 @@ export type ModelMatchProps = {
|
||||
model: string;
|
||||
};
|
||||
|
||||
const MODEL_MATCH_CACHE_LOCKED_KEY = "LOCK:model-match-clear";
|
||||
|
||||
export async function findModel(p: ModelMatchProps): Promise<Model | null> {
|
||||
return instrumentAsync(
|
||||
{
|
||||
@@ -78,6 +80,14 @@ const getModelFromRedis = async (
|
||||
}
|
||||
|
||||
try {
|
||||
if (await isModelMatchCacheLocked()) {
|
||||
logger.info(
|
||||
"Model match cache is locked. Skipping model lookup from Redis.",
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = getRedisModelKey(p);
|
||||
const redisModel = await redis?.get(key);
|
||||
if (redisModel) {
|
||||
@@ -241,6 +251,70 @@ export async function clearModelCacheForProject(
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error clearing model cache for project ${projectId}`, error);
|
||||
logger.error(
|
||||
`Error clearing model cache for project ${projectId}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function isModelMatchCacheLocked() {
|
||||
try {
|
||||
return Boolean(await redis?.exists(MODEL_MATCH_CACHE_LOCKED_KEY));
|
||||
} catch (err) {
|
||||
logger.error("Failed to check whether model match is locked", err);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearFullModelCache() {
|
||||
if (env.LANGFUSE_CACHE_MODEL_MATCH_ENABLED === "false" || !redis) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use lock to protect for concurrent executions
|
||||
// This function is called on worker startup, so we want to avoid all workers triggering this delete
|
||||
if (await isModelMatchCacheLocked()) {
|
||||
logger.info("Model cache clearing already in progress; skipping.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
logger.info("Clearing full model cache...");
|
||||
|
||||
const tenMinutesInSeconds = 60 * 10;
|
||||
await redis.setex(
|
||||
MODEL_MATCH_CACHE_LOCKED_KEY,
|
||||
tenMinutesInSeconds,
|
||||
"locked",
|
||||
);
|
||||
|
||||
const pattern = getModelMatchKeyPrefix() + "*";
|
||||
|
||||
const keys =
|
||||
env.REDIS_CLUSTER_ENABLED === "true"
|
||||
? (
|
||||
await Promise.all(
|
||||
(redis as Cluster)
|
||||
.nodes("master")
|
||||
.map((node) => node.keys(pattern) || []),
|
||||
)
|
||||
).flat()
|
||||
: await redis.keys(pattern);
|
||||
|
||||
if (keys.length > 0) {
|
||||
await safeMultiDel(redis, keys);
|
||||
logger.info(
|
||||
`Cleared full model cache with ${keys.length} keys in ${Date.now() - startTime}ms.`,
|
||||
);
|
||||
} else {
|
||||
logger.info(`No keys found for match pattern '${pattern}'`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error clearing full model cache: ${error}`);
|
||||
} finally {
|
||||
await redis?.del(MODEL_MATCH_CACHE_LOCKED_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +223,13 @@ export const eventTypes = {
|
||||
SPAN_UPDATE: "span-update",
|
||||
GENERATION_CREATE: "generation-create",
|
||||
GENERATION_UPDATE: "generation-update",
|
||||
AGENT_CREATE: "agent-create",
|
||||
TOOL_CREATE: "tool-create",
|
||||
CHAIN_CREATE: "chain-create",
|
||||
RETRIEVER_CREATE: "retriever-create",
|
||||
EVALUATOR_CREATE: "evaluator-create",
|
||||
EMBEDDING_CREATE: "embedding-create",
|
||||
GUARDRAIL_CREATE: "guardrail-create",
|
||||
SDK_LOG: "sdk-log",
|
||||
DATASET_RUN_ITEM_CREATE: "dataset-run-item-create",
|
||||
// LEGACY, only required for backwards compatibility
|
||||
@@ -564,6 +571,41 @@ const createAllIngestionSchemas = ({
|
||||
body: UpdateGenerationBody,
|
||||
});
|
||||
|
||||
const agentCreateEvent = base.extend({
|
||||
type: z.literal(eventTypes.AGENT_CREATE),
|
||||
body: CreateGenerationBody,
|
||||
});
|
||||
|
||||
const toolCreateEvent = base.extend({
|
||||
type: z.literal(eventTypes.TOOL_CREATE),
|
||||
body: CreateGenerationBody,
|
||||
});
|
||||
|
||||
const chainCreateEvent = base.extend({
|
||||
type: z.literal(eventTypes.CHAIN_CREATE),
|
||||
body: CreateGenerationBody,
|
||||
});
|
||||
|
||||
const retrieverCreateEvent = base.extend({
|
||||
type: z.literal(eventTypes.RETRIEVER_CREATE),
|
||||
body: CreateGenerationBody,
|
||||
});
|
||||
|
||||
const evaluatorCreateEvent = base.extend({
|
||||
type: z.literal(eventTypes.EVALUATOR_CREATE),
|
||||
body: CreateGenerationBody,
|
||||
});
|
||||
|
||||
const embeddingCreateEvent = base.extend({
|
||||
type: z.literal(eventTypes.EMBEDDING_CREATE),
|
||||
body: CreateGenerationBody,
|
||||
});
|
||||
|
||||
const guardrailCreateEvent = base.extend({
|
||||
type: z.literal(eventTypes.GUARDRAIL_CREATE),
|
||||
body: CreateGenerationBody,
|
||||
});
|
||||
|
||||
const scoreEvent = base.extend({
|
||||
type: z.literal(eventTypes.SCORE_CREATE),
|
||||
body: ScoreBody,
|
||||
@@ -603,6 +645,13 @@ const createAllIngestionSchemas = ({
|
||||
spanUpdateEvent,
|
||||
generationCreateEvent,
|
||||
generationUpdateEvent,
|
||||
agentCreateEvent,
|
||||
toolCreateEvent,
|
||||
chainCreateEvent,
|
||||
retrieverCreateEvent,
|
||||
evaluatorCreateEvent,
|
||||
embeddingCreateEvent,
|
||||
guardrailCreateEvent,
|
||||
sdkLogEvent,
|
||||
datasetRunItemCreateEvent,
|
||||
// LEGACY, only required for backwards compatibility
|
||||
@@ -629,6 +678,13 @@ const createAllIngestionSchemas = ({
|
||||
spanUpdateEvent,
|
||||
generationCreateEvent,
|
||||
generationUpdateEvent,
|
||||
agentCreateEvent,
|
||||
toolCreateEvent,
|
||||
chainCreateEvent,
|
||||
retrieverCreateEvent,
|
||||
evaluatorCreateEvent,
|
||||
embeddingCreateEvent,
|
||||
guardrailCreateEvent,
|
||||
scoreEvent,
|
||||
datasetRunItemCreateEvent,
|
||||
sdkLogEvent,
|
||||
@@ -666,6 +722,13 @@ export const spanCreateEvent = publicSchemas.spanCreateEvent;
|
||||
export const spanUpdateEvent = publicSchemas.spanUpdateEvent;
|
||||
export const generationCreateEvent = publicSchemas.generationCreateEvent;
|
||||
export const generationUpdateEvent = publicSchemas.generationUpdateEvent;
|
||||
export const agentCreateEvent = publicSchemas.agentCreateEvent;
|
||||
export const toolCreateEvent = publicSchemas.toolCreateEvent;
|
||||
export const chainCreateEvent = publicSchemas.chainCreateEvent;
|
||||
export const retrieverCreateEvent = publicSchemas.retrieverCreateEvent;
|
||||
export const evaluatorCreateEvent = publicSchemas.evaluatorCreateEvent;
|
||||
export const embeddingCreateEvent = publicSchemas.embeddingCreateEvent;
|
||||
export const guardrailCreateEvent = publicSchemas.guardrailCreateEvent;
|
||||
export const scoreEvent = publicSchemas.scoreEvent;
|
||||
export const sdkLogEvent = publicSchemas.sdkLogEvent;
|
||||
export const datasetRunItemCreateEvent =
|
||||
@@ -707,4 +770,11 @@ export type ObservationEvent =
|
||||
| z.infer<typeof spanCreateEvent>
|
||||
| z.infer<typeof spanUpdateEvent>
|
||||
| z.infer<typeof generationCreateEvent>
|
||||
| z.infer<typeof generationUpdateEvent>;
|
||||
| z.infer<typeof generationUpdateEvent>
|
||||
| z.infer<typeof agentCreateEvent>
|
||||
| z.infer<typeof toolCreateEvent>
|
||||
| z.infer<typeof chainCreateEvent>
|
||||
| z.infer<typeof retrieverCreateEvent>
|
||||
| z.infer<typeof evaluatorCreateEvent>
|
||||
| z.infer<typeof embeddingCreateEvent>
|
||||
| z.infer<typeof guardrailCreateEvent>;
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
} from "./types";
|
||||
import { CallbackHandler } from "langfuse-langchain";
|
||||
import type { BaseCallbackHandler } from "@langchain/core/callbacks/base";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
|
||||
const isLangfuseCloud = Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION);
|
||||
|
||||
@@ -217,6 +218,10 @@ export async function fetchLLMCompletion(
|
||||
(m) => m.content.length > 0 || "tool_calls" in m,
|
||||
);
|
||||
|
||||
// Common proxy configuration for all adapters
|
||||
const proxyUrl = env.HTTPS_PROXY;
|
||||
const proxyAgent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : undefined;
|
||||
|
||||
let chatModel:
|
||||
| ChatOpenAI
|
||||
| ChatAnthropic
|
||||
@@ -232,7 +237,12 @@ export async function fetchLLMCompletion(
|
||||
maxTokens: modelParams.max_tokens,
|
||||
topP: modelParams.top_p,
|
||||
callbacks: finalCallbacks,
|
||||
clientOptions: { maxRetries, timeout: 1000 * 60 * 2 }, // 2 minutes timeout
|
||||
clientOptions: {
|
||||
maxRetries,
|
||||
timeout: 1000 * 60 * 2, // 2 minutes timeout
|
||||
...(proxyAgent && { httpAgent: proxyAgent }),
|
||||
},
|
||||
invocationKwargs: modelParams.providerOptions,
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.OpenAI) {
|
||||
chatModel = new ChatOpenAI({
|
||||
@@ -247,7 +257,9 @@ export async function fetchLLMCompletion(
|
||||
configuration: {
|
||||
baseURL,
|
||||
defaultHeaders: extraHeaders,
|
||||
...(proxyAgent && { httpAgent: proxyAgent }),
|
||||
},
|
||||
modelKwargs: modelParams.providerOptions,
|
||||
timeout: 1000 * 60 * 2, // 2 minutes timeout
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.Azure) {
|
||||
@@ -264,7 +276,9 @@ export async function fetchLLMCompletion(
|
||||
timeout: 1000 * 60 * 2, // 2 minutes timeout
|
||||
configuration: {
|
||||
defaultHeaders: extraHeaders,
|
||||
...(proxyAgent && { httpAgent: proxyAgent }),
|
||||
},
|
||||
modelKwargs: modelParams.providerOptions,
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.Bedrock) {
|
||||
const { region } = BedrockConfigSchema.parse(config);
|
||||
@@ -284,6 +298,7 @@ export async function fetchLLMCompletion(
|
||||
callbacks: finalCallbacks,
|
||||
maxRetries,
|
||||
timeout: 1000 * 60 * 2, // 2 minutes timeout
|
||||
additionalModelRequestFields: modelParams.providerOptions as any,
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.VertexAI) {
|
||||
const credentials = GCPServiceAccountKeySchema.parse(JSON.parse(apiKey));
|
||||
@@ -340,51 +355,6 @@ export async function fetchLLMCompletion(
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
Workaround OpenAI reasoning models:
|
||||
|
||||
This is a temporary workaround to avoid sending unsupported parameters to OpenAI's O1 models.
|
||||
O1 models do not support:
|
||||
- system messages
|
||||
- top_p
|
||||
- max_tokens at all, one has to use max_completion_tokens instead
|
||||
- temperature different than 1
|
||||
|
||||
Reference: https://platform.openai.com/docs/guides/reasoning/beta-limitations
|
||||
*/
|
||||
if (
|
||||
modelParams.model.startsWith("o1-") ||
|
||||
modelParams.model.startsWith("o3-")
|
||||
) {
|
||||
const filteredMessages = finalMessages.filter((message) => {
|
||||
return (
|
||||
modelParams.model.startsWith("o3-") || message._getType() !== "system"
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
completion: await new ChatOpenAI({
|
||||
openAIApiKey: apiKey,
|
||||
modelName: modelParams.model,
|
||||
temperature: 1,
|
||||
maxTokens: undefined,
|
||||
topP: undefined,
|
||||
callbacks,
|
||||
maxRetries,
|
||||
modelKwargs: {
|
||||
max_completion_tokens: modelParams.max_tokens,
|
||||
},
|
||||
configuration: {
|
||||
baseURL,
|
||||
},
|
||||
timeout: 1000 * 60 * 2, // 2 minutes timeout
|
||||
})
|
||||
.pipe(new StringOutputParser())
|
||||
.invoke(filteredMessages, runConfig),
|
||||
processTracedEvents,
|
||||
};
|
||||
}
|
||||
|
||||
if (tools && tools.length > 0) {
|
||||
const langchainTools = tools.map((tool) => ({
|
||||
type: "function",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { z as zodV3 } from "zod/v3";
|
||||
import {
|
||||
ChatMessageRole,
|
||||
ChatMessageType,
|
||||
LLMApiKeySchema,
|
||||
type ModelConfig,
|
||||
} from "./types";
|
||||
import { decrypt } from "../../encryption";
|
||||
import { fetchLLMCompletion } from "./fetchLLMCompletion";
|
||||
import { decryptAndParseExtraHeaders } from "./utils";
|
||||
import z from "zod/v4";
|
||||
|
||||
export const testModelCall = async ({
|
||||
provider,
|
||||
model,
|
||||
apiKey,
|
||||
prompt,
|
||||
modelConfig,
|
||||
}: {
|
||||
provider: string;
|
||||
model: string;
|
||||
apiKey: z.infer<typeof LLMApiKeySchema>;
|
||||
prompt?: string;
|
||||
modelConfig?: ModelConfig | null;
|
||||
}) => {
|
||||
(
|
||||
await fetchLLMCompletion({
|
||||
streaming: false,
|
||||
apiKey: decrypt(apiKey.secretKey), // decrypt the secret key
|
||||
extraHeaders: decryptAndParseExtraHeaders(apiKey.extraHeaders),
|
||||
baseURL: apiKey.baseURL ?? undefined,
|
||||
messages: [
|
||||
{
|
||||
role: ChatMessageRole.User,
|
||||
content: prompt ?? "mock content",
|
||||
type: ChatMessageType.User,
|
||||
},
|
||||
],
|
||||
modelParams: {
|
||||
provider: provider,
|
||||
model: model,
|
||||
adapter: apiKey.adapter,
|
||||
...modelConfig,
|
||||
},
|
||||
structuredOutputSchema: zodV3.object({
|
||||
score: zodV3.string(),
|
||||
reasoning: zodV3.string(),
|
||||
}),
|
||||
config: apiKey.config,
|
||||
})
|
||||
).completion;
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "../../interfaces/customLLMProviderConfigSchemas";
|
||||
import { TokenCountDelegate } from "../ingestion/processEventBatch";
|
||||
import { AuthHeaderValidVerificationResult } from "../auth/types";
|
||||
import { JSONObjectSchema } from "../../utils/zod";
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
// disable lint as this is exported and used in web/worker
|
||||
@@ -271,6 +272,7 @@ export const ZodModelConfig = z.object({
|
||||
max_tokens: z.coerce.number().optional(),
|
||||
temperature: z.coerce.number().optional(),
|
||||
top_p: z.coerce.number().optional(),
|
||||
providerOptions: JSONObjectSchema.optional(),
|
||||
});
|
||||
|
||||
// Experiment config
|
||||
@@ -294,6 +296,12 @@ export const openAIModels = [
|
||||
"gpt-4.1-mini-2025-04-14",
|
||||
"gpt-4.1-nano",
|
||||
"gpt-4.1-nano-2025-04-14",
|
||||
"gpt-5",
|
||||
"gpt-5-2025-08-07",
|
||||
"gpt-5-mini",
|
||||
"gpt-5-mini-2025-08-07",
|
||||
"gpt-5-nano",
|
||||
"gpt-5-nano-2025-08-07",
|
||||
"o3",
|
||||
"o3-2025-04-16",
|
||||
"o4-mini",
|
||||
|
||||
@@ -6,7 +6,6 @@ import { DatasetRunItemUpsertQueue } from "./datasetRunItemUpsert";
|
||||
import { EvalExecutionQueue } from "./evalExecutionQueue";
|
||||
import { ExperimentCreateQueue } from "./experimentCreateQueue";
|
||||
import { SecondaryIngestionQueue } from "./ingestionQueue";
|
||||
import { TraceUpsertQueue } from "./traceUpsert";
|
||||
import { TraceDeleteQueue } from "./traceDelete";
|
||||
import { ProjectDeleteQueue } from "./projectDelete";
|
||||
import { PostHogIntegrationQueue } from "./postHogIntegrationQueue";
|
||||
@@ -25,10 +24,13 @@ import { WebhookQueue } from "./webhookQueue";
|
||||
import { EntityChangeQueue } from "./entityChangeQueue";
|
||||
import { DatasetDeleteQueue } from "./datasetDelete";
|
||||
|
||||
// IngestionQueue is sharded and requires a sharding key
|
||||
// Use IngestionQueue.getInstance({ shardName: queueName }) directly instead
|
||||
// IngestionQueue and TraceUpsert are sharded and require a sharding key
|
||||
// Use IngestionQueue.getInstance({ shardName: queueName }) or TraceUpsertQueue.getInstance({ shardName: queueName }) directly instead
|
||||
export function getQueue(
|
||||
queueName: Exclude<QueueName, QueueName.IngestionQueue>,
|
||||
queueName: Exclude<
|
||||
QueueName,
|
||||
QueueName.IngestionQueue | QueueName.TraceUpsert
|
||||
>,
|
||||
): Queue | null {
|
||||
switch (queueName) {
|
||||
case QueueName.BatchExport:
|
||||
@@ -43,8 +45,6 @@ export function getQueue(
|
||||
return EvalExecutionQueue.getInstance();
|
||||
case QueueName.ExperimentCreate:
|
||||
return ExperimentCreateQueue.getInstance();
|
||||
case QueueName.TraceUpsert:
|
||||
return TraceUpsertQueue.getInstance();
|
||||
case QueueName.TraceDelete:
|
||||
return TraceDeleteQueue.getInstance();
|
||||
case QueueName.ProjectDelete:
|
||||
|
||||
@@ -6,45 +6,92 @@ import {
|
||||
getQueuePrefix,
|
||||
} from "./redis";
|
||||
import { logger } from "../logger";
|
||||
import { getShardIndex } from "./sharding";
|
||||
import { env } from "../../env";
|
||||
|
||||
export class TraceUpsertQueue {
|
||||
private static instance: Queue<TQueueJobTypes[QueueName.TraceUpsert]> | null =
|
||||
null;
|
||||
private static instances: Map<
|
||||
number,
|
||||
Queue<TQueueJobTypes[QueueName.TraceUpsert]> | null
|
||||
> = new Map();
|
||||
|
||||
public static getInstance(): Queue<
|
||||
TQueueJobTypes[QueueName.TraceUpsert]
|
||||
> | null {
|
||||
if (TraceUpsertQueue.instance) return TraceUpsertQueue.instance;
|
||||
public static getShardNames() {
|
||||
return Array.from(
|
||||
{ length: env.LANGFUSE_TRACE_UPSERT_QUEUE_SHARD_COUNT },
|
||||
(_, i) => `${QueueName.TraceUpsert}${i > 0 ? `-${i}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
static getShardIndexFromShardName(
|
||||
shardName: string | undefined,
|
||||
): number | null {
|
||||
if (!shardName) return null;
|
||||
|
||||
// Extract shard index from shard name
|
||||
const shardIndex =
|
||||
shardName === QueueName.TraceUpsert
|
||||
? 0
|
||||
: parseInt(shardName.replace(`${QueueName.TraceUpsert}-`, ""), 10);
|
||||
|
||||
if (isNaN(shardIndex)) return null;
|
||||
return shardIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the trace upsert queue instance for the given sharding key or shard name.
|
||||
* @param shardingKey - ShardingKey is being hashed and randomly allocated to a shard. Should be `projectId-traceId`.
|
||||
* @param shardName - Name of the shard. Should be `trace-upsert-queue-${shardIndex}` or plainly `trace-upsert-queue` for the first shard.
|
||||
*/
|
||||
public static getInstance({
|
||||
shardingKey,
|
||||
shardName,
|
||||
}: {
|
||||
shardingKey?: string;
|
||||
shardName?: string;
|
||||
} = {}): Queue<TQueueJobTypes[QueueName.TraceUpsert]> | null {
|
||||
const shardIndex =
|
||||
TraceUpsertQueue.getShardIndexFromShardName(shardName) ??
|
||||
(env.REDIS_CLUSTER_ENABLED === "true" && shardingKey
|
||||
? getShardIndex(
|
||||
shardingKey,
|
||||
env.LANGFUSE_TRACE_UPSERT_QUEUE_SHARD_COUNT,
|
||||
)
|
||||
: 0);
|
||||
|
||||
// Check if we already have an instance for this shard
|
||||
if (TraceUpsertQueue.instances.has(shardIndex)) {
|
||||
return TraceUpsertQueue.instances.get(shardIndex) || null;
|
||||
}
|
||||
|
||||
const newRedis = createNewRedisInstance({
|
||||
enableOfflineQueue: false,
|
||||
...redisQueueRetryOptions,
|
||||
});
|
||||
|
||||
TraceUpsertQueue.instance = newRedis
|
||||
? new Queue<TQueueJobTypes[QueueName.TraceUpsert]>(
|
||||
QueueName.TraceUpsert,
|
||||
{
|
||||
connection: newRedis,
|
||||
prefix: getQueuePrefix(QueueName.TraceUpsert),
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: 100, // Important: If not true, new jobs for that ID would be ignored as jobs in the complete set are still considered as part of the queue
|
||||
removeOnFail: 100_000,
|
||||
attempts: 5,
|
||||
delay: 15_000, // 15 seconds
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
const name = `${QueueName.TraceUpsert}${shardIndex > 0 ? `-${shardIndex}` : ""}`;
|
||||
const queueInstance = newRedis
|
||||
? new Queue<TQueueJobTypes[QueueName.TraceUpsert]>(name, {
|
||||
connection: newRedis,
|
||||
prefix: getQueuePrefix(name),
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: 100,
|
||||
removeOnFail: 100_000,
|
||||
attempts: 5,
|
||||
delay: 15_000, // 15 seconds
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
: null;
|
||||
|
||||
TraceUpsertQueue.instance?.on("error", (err) => {
|
||||
logger.error("TraceUpsertQueue error", err);
|
||||
queueInstance?.on("error", (err) => {
|
||||
logger.error(`TraceUpsertQueue shard ${shardIndex} error`, err);
|
||||
});
|
||||
|
||||
return TraceUpsertQueue.instance;
|
||||
TraceUpsertQueue.instances.set(shardIndex, queueInstance);
|
||||
|
||||
return queueInstance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,13 +78,13 @@ const getProjectDatasetIdDefaultFilter = (
|
||||
return {
|
||||
datasetRunItemsFilter: new FilterList([
|
||||
new StringFilter({
|
||||
clickhouseTable: "dataset_run_items",
|
||||
clickhouseTable: "dataset_run_items_rmt",
|
||||
field: "project_id",
|
||||
operator: "=",
|
||||
value: projectId,
|
||||
}),
|
||||
new StringFilter({
|
||||
clickhouseTable: "dataset_run_items",
|
||||
clickhouseTable: "dataset_run_items_rmt",
|
||||
field: "dataset_id",
|
||||
operator: "=",
|
||||
value: datasetId,
|
||||
@@ -132,13 +132,13 @@ const getDatasetRunsTableInternal = async <T>(
|
||||
WHERE o.project_id = {projectId: String}
|
||||
AND o.start_time >= (
|
||||
SELECT min(dri.dataset_run_created_at) - INTERVAL 1 DAY
|
||||
FROM dataset_run_items dri
|
||||
FROM dataset_run_items_rmt dri
|
||||
WHERE dri.project_id = {projectId: String}
|
||||
AND dri.dataset_id = {datasetId: String}
|
||||
)
|
||||
AND o.start_time <= (
|
||||
SELECT max(dri.dataset_run_created_at) + INTERVAL 1 DAY
|
||||
FROM dataset_run_items dri
|
||||
FROM dataset_run_items_rmt dri
|
||||
WHERE dri.project_id = {projectId: String}
|
||||
AND dri.dataset_id = {datasetId: String}
|
||||
)
|
||||
@@ -150,7 +150,7 @@ const getDatasetRunsTableInternal = async <T>(
|
||||
dateDiff('millisecond', min(of.start_time), max(of.end_time)) as latency_ms,
|
||||
sum(of.total_cost) as total_cost
|
||||
FROM observations_filtered of
|
||||
JOIN dataset_run_items dri ON dri.trace_id = of.trace_id
|
||||
JOIN dataset_run_items_rmt dri ON dri.trace_id = of.trace_id
|
||||
AND dri.project_id = of.project_id
|
||||
AND dri.observation_id IS NULL -- Only for trace-level dataset run items
|
||||
WHERE dri.dataset_id = {datasetId: String}
|
||||
@@ -163,7 +163,7 @@ const getDatasetRunsTableInternal = async <T>(
|
||||
dri.trace_id,
|
||||
of.total_cost,
|
||||
dateDiff('millisecond', of.start_time, of.end_time) as latency_ms
|
||||
FROM dataset_run_items dri
|
||||
FROM dataset_run_items_rmt dri
|
||||
JOIN observations_filtered of ON dri.observation_id = of.id
|
||||
AND dri.project_id = of.project_id
|
||||
AND dri.trace_id = of.trace_id
|
||||
@@ -190,7 +190,7 @@ const getDatasetRunsTableInternal = async <T>(
|
||||
THEN od.total_cost
|
||||
ELSE COALESCE(ta.total_cost, 0)
|
||||
END) as avg_total_cost
|
||||
FROM dataset_run_items dri
|
||||
FROM dataset_run_items_rmt dri
|
||||
LEFT JOIN traces_aggregated ta
|
||||
ON dri.trace_id = ta.trace_id
|
||||
AND dri.project_id = ta.project_id
|
||||
@@ -317,7 +317,7 @@ const getDatasetRunItemsTableInternal = async <T>(
|
||||
const query = `
|
||||
SELECT
|
||||
${selectString}
|
||||
FROM dataset_run_items dri
|
||||
FROM dataset_run_items_rmt dri
|
||||
WHERE ${appliedFilter.query}
|
||||
${orderByClause}
|
||||
${opts.select === "rows" ? "LIMIT 1 BY dri.project_id, dri.dataset_id, dri.dataset_run_id, dri.dataset_item_id" : ""}
|
||||
@@ -371,7 +371,7 @@ export const deleteDatasetRunItemsByProjectId = async ({
|
||||
projectId: string;
|
||||
}) => {
|
||||
const query = `
|
||||
DELETE FROM dataset_run_items
|
||||
DELETE FROM dataset_run_items_rmt
|
||||
WHERE project_id = {projectId: String};
|
||||
`;
|
||||
await commandClickhouse({
|
||||
@@ -399,7 +399,7 @@ export const deleteDatasetRunItemsByDatasetId = async ({
|
||||
datasetId: string;
|
||||
}) => {
|
||||
const query = `
|
||||
DELETE FROM dataset_run_items
|
||||
DELETE FROM dataset_run_items_rmt
|
||||
WHERE project_id = {projectId: String}
|
||||
AND dataset_id = {datasetId: String}
|
||||
`;
|
||||
@@ -432,7 +432,7 @@ export const deleteDatasetRunItemsByDatasetRunIds = async ({
|
||||
datasetId: string;
|
||||
}) => {
|
||||
const query = `
|
||||
DELETE FROM dataset_run_items
|
||||
DELETE FROM dataset_run_items_rmt
|
||||
WHERE project_id = {projectId: String}
|
||||
AND dataset_id = {datasetId: String}
|
||||
AND dataset_run_id IN ({datasetRunIds: Array(String)})
|
||||
|
||||
@@ -1508,6 +1508,15 @@ export const getGenerationsForPostHog = async function* (
|
||||
minTimestamp: Date,
|
||||
maxTimestamp: Date,
|
||||
) {
|
||||
// Determine which trace table to use based on experiment flag
|
||||
const useAMT = env.LANGFUSE_EXPERIMENT_RETURN_NEW_RESULT === "true";
|
||||
// Subtract 7d from minTimestamp to account for shift in query
|
||||
const traceTable = useAMT
|
||||
? getTimeframesTracesAMT(
|
||||
new Date(minTimestamp.getTime() - 7 * 24 * 60 * 60 * 1000),
|
||||
)
|
||||
: "traces";
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
o.name as name,
|
||||
@@ -1532,7 +1541,7 @@ export const getGenerationsForPostHog = async function* (
|
||||
t.tags as trace_tags,
|
||||
t.metadata['$posthog_session_id'] as posthog_session_id
|
||||
FROM observations o FINAL
|
||||
LEFT JOIN traces t FINAL ON o.trace_id = t.id AND o.project_id = t.project_id
|
||||
LEFT JOIN ${traceTable} t FINAL ON o.trace_id = t.id AND o.project_id = t.project_id
|
||||
WHERE o.project_id = {projectId: String}
|
||||
AND t.project_id = {projectId: String}
|
||||
AND o.start_time >= {minTimestamp: DateTime64(3)}
|
||||
@@ -1554,6 +1563,7 @@ export const getGenerationsForPostHog = async function* (
|
||||
type: "observation",
|
||||
kind: "analytic",
|
||||
projectId,
|
||||
experiment_amt: useAMT ? "new" : "original",
|
||||
},
|
||||
clickhouseConfigs: {
|
||||
request_timeout: env.LANGFUSE_CLICKHOUSE_DATA_EXPORT_REQUEST_TIMEOUT_MS,
|
||||
|
||||
@@ -33,6 +33,7 @@ import { ClickHouseClientConfigOptions } from "@clickhouse/client";
|
||||
import { recordDistribution } from "../instrumentation";
|
||||
import { prisma } from "../../db";
|
||||
import { measureAndReturn } from "../clickhouse/measureAndReturn";
|
||||
import { getTimeframesTracesAMT } from "./traces";
|
||||
|
||||
export const searchExistingAnnotationScore = async (
|
||||
projectId: string,
|
||||
@@ -296,7 +297,7 @@ export const getTraceScoresForDatasetRuns = async (
|
||||
s.* EXCEPT (metadata),
|
||||
length(mapKeys(s.metadata)) > 0 AS has_metadata,
|
||||
dri.dataset_run_id as run_id
|
||||
FROM dataset_run_items dri
|
||||
FROM dataset_run_items_rmt dri
|
||||
JOIN scores s FINAL ON dri.trace_id = s.trace_id
|
||||
AND dri.project_id = s.project_id
|
||||
WHERE dri.project_id = {projectId: String}
|
||||
@@ -1152,7 +1153,7 @@ export const getNumericScoreHistogram = async (
|
||||
const query = `
|
||||
select s.value
|
||||
from scores s
|
||||
${traceFilter ? `LEFT JOIN traces t ON s.trace_id = t.id AND t.project_id = s.project_id` : ""}
|
||||
${traceFilter ? `LEFT JOIN __TRACE_TABLE__ t ON s.trace_id = t.id AND t.project_id = s.project_id` : ""}
|
||||
WHERE s.project_id = {projectId: String}
|
||||
${traceFilter ? `AND t.project_id = {projectId: String}` : ""}
|
||||
${chFilterRes?.query ? `AND ${chFilterRes.query}` : ""}
|
||||
@@ -1161,18 +1162,45 @@ export const getNumericScoreHistogram = async (
|
||||
${limit !== undefined ? `limit {limit: Int32}` : ""}
|
||||
`;
|
||||
|
||||
return queryClickhouse<{ value: number }>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
limit,
|
||||
...(chFilterRes ? chFilterRes.params : {}),
|
||||
// Extract timestamp from filter for AMT table selection
|
||||
const timestampFilter = chFilter.find(
|
||||
(f) => f.clickhouseTable === "traces" && f.field === "timestamp",
|
||||
) as TimeFilter | undefined;
|
||||
const timestamp = timestampFilter?.value;
|
||||
|
||||
return measureAndReturn({
|
||||
operationName: "getNumericScoreHistogram",
|
||||
projectId,
|
||||
minStartTime: timestamp,
|
||||
input: {
|
||||
params: {
|
||||
projectId,
|
||||
limit,
|
||||
...(chFilterRes ? chFilterRes.params : {}),
|
||||
},
|
||||
tags: {
|
||||
feature: "tracing",
|
||||
type: "score",
|
||||
kind: "analytic",
|
||||
projectId,
|
||||
operation_name: "getNumericScoreHistogram",
|
||||
},
|
||||
timestamp,
|
||||
},
|
||||
tags: {
|
||||
feature: "tracing",
|
||||
type: "score",
|
||||
kind: "analytic",
|
||||
projectId,
|
||||
existingExecution: async (input) => {
|
||||
return queryClickhouse<{ value: number }>({
|
||||
query: query.replace("__TRACE_TABLE__", "traces"),
|
||||
params: input.params,
|
||||
tags: { ...input.tags, experiment_amt: "original" },
|
||||
});
|
||||
},
|
||||
newExecution: async (input) => {
|
||||
const traceAmt = getTimeframesTracesAMT(input.timestamp);
|
||||
return queryClickhouse<{ value: number }>({
|
||||
query: query.replace("__TRACE_TABLE__", traceAmt),
|
||||
params: input.params,
|
||||
tags: { ...input.tags, experiment_amt: "new" },
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1399,6 +1427,15 @@ export const getScoresForPostHog = async function* (
|
||||
minTimestamp: Date,
|
||||
maxTimestamp: Date,
|
||||
) {
|
||||
// Determine which trace table to use based on experiment flag
|
||||
const useAMT = env.LANGFUSE_EXPERIMENT_RETURN_NEW_RESULT === "true";
|
||||
// Subtract 7d from minTimestamp to account for shift in query
|
||||
const traceTable = useAMT
|
||||
? getTimeframesTracesAMT(
|
||||
new Date(minTimestamp.getTime() - 7 * 24 * 60 * 60 * 1000),
|
||||
)
|
||||
: "traces";
|
||||
|
||||
const query = ` SELECT
|
||||
s.id as id,
|
||||
s.timestamp as timestamp,
|
||||
@@ -1417,7 +1454,7 @@ export const getScoresForPostHog = async function* (
|
||||
s.metadata as metadata,
|
||||
t.metadata['$posthog_session_id'] as posthog_session_id
|
||||
FROM scores s FINAL
|
||||
LEFT JOIN traces t FINAL ON s.trace_id = t.id AND s.project_id = t.project_id
|
||||
LEFT JOIN ${traceTable} t FINAL ON s.trace_id = t.id AND s.project_id = t.project_id
|
||||
WHERE s.project_id = {projectId: String}
|
||||
AND t.project_id = {projectId: String}
|
||||
AND s.timestamp >= {minTimestamp: DateTime64(3)}
|
||||
@@ -1438,6 +1475,7 @@ export const getScoresForPostHog = async function* (
|
||||
type: "score",
|
||||
kind: "analytic",
|
||||
projectId,
|
||||
experiment_amt: useAMT ? "new" : "original",
|
||||
},
|
||||
clickhouseConfigs: {
|
||||
request_timeout: env.LANGFUSE_CLICKHOUSE_DATA_EXPORT_REQUEST_TIMEOUT_MS,
|
||||
|
||||
@@ -47,12 +47,16 @@ enum TracesAMTs {
|
||||
* for <= 29 days, we use traces_30d_amt,
|
||||
* for all other cases we use traces_all_amt.
|
||||
*
|
||||
* If LANGFUSE_EXPERIMENT_WHITELISTED_AMT_TABLES is set, we only return timeframes
|
||||
* that are whitelisted or fallback to the traces_all_amt.
|
||||
*
|
||||
* @param fromTimestamp
|
||||
*/
|
||||
export const getTimeframesTracesAMT = (
|
||||
fromTimestamp: Date | undefined,
|
||||
): TracesAMTs => {
|
||||
if (!fromTimestamp) {
|
||||
// The TracesAllAMT must always be returned if there is no timestamp.
|
||||
return TracesAMTs.TracesAllAMT;
|
||||
}
|
||||
|
||||
@@ -60,12 +64,21 @@ export const getTimeframesTracesAMT = (
|
||||
const diffInDays = Math.floor(
|
||||
(now.getTime() - fromTimestamp.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
|
||||
let selectedTable: TracesAMTs;
|
||||
if (diffInDays <= 6) {
|
||||
return TracesAMTs.Traces7dAMT;
|
||||
selectedTable = TracesAMTs.Traces7dAMT;
|
||||
} else if (diffInDays <= 29) {
|
||||
return TracesAMTs.Traces30dAMT;
|
||||
selectedTable = TracesAMTs.Traces30dAMT;
|
||||
} else {
|
||||
selectedTable = TracesAMTs.TracesAllAMT;
|
||||
}
|
||||
return TracesAMTs.TracesAllAMT;
|
||||
|
||||
// Check if the selected table is whitelisted, fallback to TracesAllAMT if not
|
||||
return env.LANGFUSE_EXPERIMENT_WHITELISTED_AMT_TABLES.length === 0 ||
|
||||
env.LANGFUSE_EXPERIMENT_WHITELISTED_AMT_TABLES.includes(selectedTable)
|
||||
? selectedTable
|
||||
: TracesAMTs.TracesAllAMT;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1184,19 +1197,6 @@ export const deleteTraces = async (projectId: string, traceIds: string[]) => {
|
||||
},
|
||||
tags: input.tags,
|
||||
}),
|
||||
// Delete from traces_null
|
||||
commandClickhouse({
|
||||
query: `
|
||||
DELETE FROM traces_null
|
||||
WHERE project_id = {projectId: String}
|
||||
AND id IN ({traceIds: Array(String)});
|
||||
`,
|
||||
params: input.params,
|
||||
clickhouseConfigs: {
|
||||
request_timeout: env.LANGFUSE_CLICKHOUSE_DELETION_TIMEOUT_MS,
|
||||
},
|
||||
tags: input.tags,
|
||||
}),
|
||||
// Delete from traces_all_amt
|
||||
commandClickhouse({
|
||||
query: `
|
||||
@@ -1377,18 +1377,6 @@ export const deleteTracesByProjectId = async (projectId: string) => {
|
||||
},
|
||||
tags: input.tags,
|
||||
}),
|
||||
// Delete from traces_null
|
||||
commandClickhouse({
|
||||
query: `
|
||||
DELETE FROM traces_null
|
||||
WHERE project_id = {projectId: String};
|
||||
`,
|
||||
params: input.params,
|
||||
clickhouseConfigs: {
|
||||
request_timeout: env.LANGFUSE_CLICKHOUSE_DELETION_TIMEOUT_MS,
|
||||
},
|
||||
tags: input.tags,
|
||||
}),
|
||||
// Delete from traces_all_amt
|
||||
commandClickhouse({
|
||||
query: `
|
||||
@@ -1761,6 +1749,10 @@ export const getTracesForBlobStorageExport = function (
|
||||
minTimestamp: Date,
|
||||
maxTimestamp: Date,
|
||||
) {
|
||||
// Determine which trace table to use based on experiment flag
|
||||
const useAMT = env.LANGFUSE_EXPERIMENT_RETURN_NEW_RESULT === "true";
|
||||
const traceTable = useAMT ? getTimeframesTracesAMT(minTimestamp) : "traces";
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
id,
|
||||
@@ -1773,12 +1765,12 @@ export const getTracesForBlobStorageExport = function (
|
||||
session_id,
|
||||
release,
|
||||
version,
|
||||
public,
|
||||
bookmarked,
|
||||
${useAMT ? "finalizeAggregation(public)" : "public"} as public,
|
||||
${useAMT ? "finalizeAggregation(bookmarked)" : "bookmarked"} as bookmarked,
|
||||
tags,
|
||||
input,
|
||||
output
|
||||
FROM traces FINAL
|
||||
${useAMT ? "finalizeAggregation(input)" : "input"} as input,
|
||||
${useAMT ? "finalizeAggregation(output)" : "output"} as output
|
||||
FROM ${traceTable} FINAL
|
||||
WHERE project_id = {projectId: String}
|
||||
AND timestamp >= {minTimestamp: DateTime64(3)}
|
||||
AND timestamp <= {maxTimestamp: DateTime64(3)}
|
||||
@@ -1796,6 +1788,7 @@ export const getTracesForBlobStorageExport = function (
|
||||
type: "trace",
|
||||
kind: "analytic",
|
||||
projectId,
|
||||
experiment_amt: useAMT ? "new" : "original",
|
||||
},
|
||||
clickhouseConfigs: {
|
||||
request_timeout: env.LANGFUSE_CLICKHOUSE_DATA_EXPORT_REQUEST_TIMEOUT_MS,
|
||||
@@ -1808,6 +1801,10 @@ export const getTracesForPostHog = async function* (
|
||||
minTimestamp: Date,
|
||||
maxTimestamp: Date,
|
||||
) {
|
||||
// Determine which trace table to use based on experiment flag
|
||||
const useAMT = env.LANGFUSE_EXPERIMENT_RETURN_NEW_RESULT === "true";
|
||||
const traceTable = useAMT ? getTimeframesTracesAMT(minTimestamp) : "traces";
|
||||
|
||||
const query = `
|
||||
WITH observations_agg AS (
|
||||
SELECT o.project_id,
|
||||
@@ -1835,7 +1832,7 @@ export const getTracesForPostHog = async function* (
|
||||
o.total_cost as total_cost,
|
||||
o.latency_milliseconds / 1000 as latency,
|
||||
o.observation_count as observation_count
|
||||
FROM traces t FINAL
|
||||
FROM ${traceTable} t FINAL
|
||||
LEFT JOIN observations_agg o ON t.id = o.trace_id AND t.project_id = o.project_id
|
||||
WHERE t.project_id = {projectId: String}
|
||||
AND t.timestamp >= {minTimestamp: DateTime64(3)}
|
||||
@@ -1854,6 +1851,7 @@ export const getTracesForPostHog = async function* (
|
||||
type: "trace",
|
||||
kind: "analytic",
|
||||
projectId,
|
||||
experiment_amt: useAMT ? "new" : "original",
|
||||
},
|
||||
clickhouseConfigs: {
|
||||
request_timeout: env.LANGFUSE_CLICKHOUSE_DATA_EXPORT_REQUEST_TIMEOUT_MS,
|
||||
|
||||
@@ -35,6 +35,12 @@ export const HistogramChartConfig = BaseTotalValueChartConfig.extend({
|
||||
|
||||
export const PivotTableChartConfig = BaseTotalValueChartConfig.extend({
|
||||
type: z.literal("PIVOT_TABLE"),
|
||||
defaultSort: z
|
||||
.object({
|
||||
column: z.string(),
|
||||
order: z.enum(["ASC", "DESC"]),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// Define dimension schema
|
||||
|
||||
+23
-1
@@ -1,7 +1,12 @@
|
||||
import z from "zod/v4";
|
||||
import { prisma } from "../../../db";
|
||||
import { LangfuseNotFoundError, QUEUE_ERROR_MESSAGES } from "../../../errors";
|
||||
import {
|
||||
ForbiddenError,
|
||||
LangfuseNotFoundError,
|
||||
QUEUE_ERROR_MESSAGES,
|
||||
} from "../../../errors";
|
||||
import { LLMApiKeySchema, ZodModelConfig } from "../../llm/types";
|
||||
import { testModelCall } from "../../llm/testModelCall";
|
||||
|
||||
type ValidConfig = {
|
||||
provider: string;
|
||||
@@ -47,6 +52,23 @@ export class DefaultEvalModelService {
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (LLMApiKeySchema.safeParse(llmApiKey).success) {
|
||||
// Make a test structured output call to validate the LLM key
|
||||
await testModelCall({
|
||||
provider,
|
||||
model,
|
||||
apiKey: llmApiKey as z.infer<typeof LLMApiKeySchema>,
|
||||
modelConfig: modelParams,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
throw new ForbiddenError(
|
||||
`Model configuration not valid for evaluation. ${message}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Create or update the default model
|
||||
return prisma.defaultLlmModel.upsert({
|
||||
where: {
|
||||
|
||||
@@ -45,7 +45,7 @@ export const createDatasetRunItemsCh = async (
|
||||
datasetRunItems: DatasetRunItemRecordInsertType[],
|
||||
) => {
|
||||
return await clickhouseClient().insert({
|
||||
table: "dataset_run_items",
|
||||
table: "dataset_run_items_rmt",
|
||||
format: "JSONEachRow",
|
||||
values: datasetRunItems,
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ export function createBasicAuthHeader(
|
||||
|
||||
export type CreateOrgProjectAndApiKeyOptions = {
|
||||
projectId?: string;
|
||||
plan?: "Team" | "Hobby" | "Core" | "Pro" | "Enterprise";
|
||||
};
|
||||
export const createOrgProjectAndApiKey = async (
|
||||
props?: CreateOrgProjectAndApiKeyOptions,
|
||||
@@ -25,7 +26,7 @@ export const createOrgProjectAndApiKey = async (
|
||||
id: v4(),
|
||||
name: v4(),
|
||||
cloudConfig: CloudConfigSchema.parse({
|
||||
plan: "Team",
|
||||
plan: props?.plan ?? "Team",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,25 +4,25 @@ export const datasetRunItemsTableUiColumnDefinitions: UiColumnMappings = [
|
||||
{
|
||||
uiTableName: "Dataset Run ID",
|
||||
uiTableId: "datasetRunId",
|
||||
clickhouseTableName: "dataset_run_items",
|
||||
clickhouseTableName: "dataset_run_items_rmt",
|
||||
clickhouseSelect: 'dri."dataset_run_id"',
|
||||
},
|
||||
{
|
||||
uiTableName: "Created At",
|
||||
uiTableId: "createdAt",
|
||||
clickhouseTableName: "dataset_run_items",
|
||||
clickhouseTableName: "dataset_run_items_rmt",
|
||||
clickhouseSelect: 'dri."created_at"',
|
||||
},
|
||||
{
|
||||
uiTableName: "Event Timestamp",
|
||||
uiTableId: "eventTs",
|
||||
clickhouseTableName: "dataset_run_items",
|
||||
clickhouseTableName: "dataset_run_items_rmt",
|
||||
clickhouseSelect: 'dri."event_ts"',
|
||||
},
|
||||
{
|
||||
uiTableName: "Dataset Item ID",
|
||||
uiTableId: "datasetItemId",
|
||||
clickhouseTableName: "dataset_run_items",
|
||||
clickhouseTableName: "dataset_run_items_rmt",
|
||||
clickhouseSelect: 'dri."dataset_item_id"',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { InputJsonValue } from "@prisma/client/runtime/library";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
// to be used for Prisma JSON type
|
||||
@@ -111,3 +112,26 @@ export const validateZodSchema = <T extends z.ZodTypeAny>(
|
||||
): z.infer<T> => {
|
||||
return schema.parse(object);
|
||||
};
|
||||
|
||||
// JSON Schema validation
|
||||
export const JSONPrimitiveValueSchema = z.union([
|
||||
z.string(),
|
||||
z.number().finite(),
|
||||
z.boolean(),
|
||||
]);
|
||||
|
||||
export const JSONValueSchema: z.ZodType<InputJsonValue> = z.lazy(() =>
|
||||
z.union([
|
||||
JSONPrimitiveValueSchema,
|
||||
z.array(JSONValueSchema),
|
||||
z.record(z.string(), JSONValueSchema),
|
||||
]),
|
||||
);
|
||||
|
||||
export const JSONObjectSchema = z.record(z.string(), JSONValueSchema);
|
||||
export const JSONArraySchema = z.array(JSONValueSchema);
|
||||
|
||||
export type JSONPrimitiveValue = z.infer<typeof JSONPrimitiveValueSchema>;
|
||||
export type JSONValue = z.infer<typeof JSONValueSchema>;
|
||||
export type JSONObject = z.infer<typeof JSONObjectSchema>;
|
||||
export type JSONArray = z.infer<typeof JSONArraySchema>;
|
||||
|
||||
Generated
+88
-147
@@ -9,6 +9,7 @@ overrides:
|
||||
katex: ^0.16.21
|
||||
tar-fs: ^2.1.2
|
||||
rollup@^4.0.0: ^4.22.4
|
||||
'@types/node-fetch': ^2.6.13
|
||||
|
||||
patchedDependencies:
|
||||
next-auth@4.24.11:
|
||||
@@ -49,9 +50,6 @@ importers:
|
||||
'@opentelemetry/api':
|
||||
specifier: '>=1.0.0 <1.10.0'
|
||||
version: 1.9.0
|
||||
axios:
|
||||
specifier: ^1.8.2
|
||||
version: 1.8.2
|
||||
https-proxy-agent:
|
||||
specifier: ^7.0.6
|
||||
version: 7.0.6
|
||||
@@ -188,17 +186,14 @@ importers:
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@slack/oauth':
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.3
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
'@slack/web-api':
|
||||
specifier: ^7.0.0
|
||||
specifier: ^7.9.3
|
||||
version: 7.9.3
|
||||
'@types/bcryptjs':
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6
|
||||
axios:
|
||||
specifier: ^1.8.2
|
||||
version: 1.8.2
|
||||
bcryptjs:
|
||||
specifier: ^2.4.3
|
||||
version: 2.4.3
|
||||
@@ -228,10 +223,10 @@ importers:
|
||||
version: 0.27.4
|
||||
langchain:
|
||||
specifier: ^0.3.28
|
||||
version: 0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.8.2)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62))
|
||||
version: 0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.11.0)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62))
|
||||
langfuse-langchain:
|
||||
specifier: 3.38.4
|
||||
version: 3.38.4(langchain@0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.8.2)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62)))
|
||||
version: 3.38.4(langchain@0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.11.0)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62)))
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@@ -518,12 +513,6 @@ importers:
|
||||
'@sentry/nextjs':
|
||||
specifier: ^8.52.0
|
||||
version: 8.55.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(next@14.2.30(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)(webpack@5.97.1)
|
||||
'@slack/oauth':
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.3
|
||||
'@slack/web-api':
|
||||
specifier: ^7.0.0
|
||||
version: 7.9.3
|
||||
'@t3-oss/env-nextjs':
|
||||
specifier: ^0.11.1
|
||||
version: 0.11.1(typescript@5.4.5)(zod@3.25.62)
|
||||
@@ -912,9 +901,6 @@ importers:
|
||||
'@prisma/instrumentation':
|
||||
specifier: ^6.3.0
|
||||
version: 6.3.0(@opentelemetry/api@1.9.0)
|
||||
'@slack/web-api':
|
||||
specifier: ^7.0.0
|
||||
version: 7.9.3
|
||||
backoff:
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
@@ -2304,6 +2290,9 @@ packages:
|
||||
'@floating-ui/core@1.7.2':
|
||||
resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==}
|
||||
|
||||
'@floating-ui/core@1.7.3':
|
||||
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
|
||||
|
||||
'@floating-ui/dom@1.6.11':
|
||||
resolution: {integrity: sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==}
|
||||
|
||||
@@ -2313,6 +2302,9 @@ packages:
|
||||
'@floating-ui/dom@1.7.2':
|
||||
resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==}
|
||||
|
||||
'@floating-ui/dom@1.7.3':
|
||||
resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==}
|
||||
|
||||
'@floating-ui/react-dom@1.3.0':
|
||||
resolution: {integrity: sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==}
|
||||
peerDependencies:
|
||||
@@ -2331,6 +2323,12 @@ packages:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@floating-ui/react-dom@2.1.5':
|
||||
resolution: {integrity: sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@floating-ui/react@0.19.2':
|
||||
resolution: {integrity: sha512-JyNk4A0Ezirq8FlXECvRtQOX/iBe5Ize0W/pLkrZjfHW9GUV7Xnq6zm6fyZuQzaHHqEnVizmvlA96e1/CkZv+w==}
|
||||
peerDependencies:
|
||||
@@ -2937,8 +2935,8 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/types@7.4.4':
|
||||
resolution: {integrity: sha512-p63yhbX52MO/ajXC7hDHJA5yjzJekvWD3q4YDLl1rSg+OXLczMYPvTuSuviPRCgRX8+E42RXz1D/dz9SxPSlWg==}
|
||||
'@mui/types@7.4.5':
|
||||
resolution: {integrity: sha512-ZPwlAOE3e8C0piCKbaabwrqZbW4QvWz0uapVPWya7fYj6PeDkl5sSJmomT7wjOcZGPB48G/a6Ubidqreptxz4g==}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
@@ -4697,26 +4695,18 @@ packages:
|
||||
'@sinonjs/fake-timers@10.3.0':
|
||||
resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==}
|
||||
|
||||
'@slack/logger@3.0.0':
|
||||
resolution: {integrity: sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA==}
|
||||
engines: {node: '>= 12.13.0', npm: '>= 6.12.0'}
|
||||
|
||||
'@slack/logger@4.0.0':
|
||||
resolution: {integrity: sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==}
|
||||
engines: {node: '>= 18', npm: '>= 8.6.0'}
|
||||
|
||||
'@slack/oauth@2.6.3':
|
||||
resolution: {integrity: sha512-1amXs6xRkJpoH6zSgjVPgGEJXCibKNff9WNDijcejIuVy1HFAl1adh7lehaGNiHhTWfQkfKxBiF+BGn56kvoFw==}
|
||||
engines: {node: '>=12.13.0', npm: '>=6.12.0'}
|
||||
'@slack/oauth@3.0.3':
|
||||
resolution: {integrity: sha512-N3pLJPacZ57bqmD1HzHDmHe/CNsL9pESZXRw7pfv6QXJVRgufPIW84aRpAez2Xb0616RpGBYZW5dZH0Nbskwyg==}
|
||||
engines: {node: '>=18', npm: '>=8.6.0'}
|
||||
|
||||
'@slack/types@2.15.0':
|
||||
resolution: {integrity: sha512-livb1gyG3J8ATLBJ3KjZfjHpTRz9btY1m5cgNuXxWJbhwRB1Gwb8Ly6XLJm2Sy1W6h+vLgqIHg7IwKrF1C1Szg==}
|
||||
engines: {node: '>= 12.13.0', npm: '>= 6.12.0'}
|
||||
|
||||
'@slack/web-api@6.13.0':
|
||||
resolution: {integrity: sha512-dv65crIgdh9ZYHrevLU6XFHTQwTyDmNqEqzuIrV+Vqe/vgiG6w37oex5ePDU1RGm2IJ90H8iOvHFvzdEO/vB+g==}
|
||||
engines: {node: '>= 12.13.0', npm: '>= 6.12.0'}
|
||||
|
||||
'@slack/web-api@7.9.3':
|
||||
resolution: {integrity: sha512-xjnoldVJyoUe61Ltqjr2UVYBolcsTpp5ottqzSI3l41UCaJgHSHIOpGuYps+nhLFvDfGGpDGUeQ7BWiq3+ypqA==}
|
||||
engines: {node: '>= 18', npm: '>= 8.6.0'}
|
||||
@@ -5400,9 +5390,6 @@ packages:
|
||||
'@types/http-errors@2.0.5':
|
||||
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
||||
|
||||
'@types/is-stream@1.1.0':
|
||||
resolution: {integrity: sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg==}
|
||||
|
||||
'@types/istanbul-lib-coverage@2.0.6':
|
||||
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
|
||||
|
||||
@@ -5424,8 +5411,8 @@ packages:
|
||||
'@types/json5@0.0.29':
|
||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||
|
||||
'@types/jsonwebtoken@8.5.9':
|
||||
resolution: {integrity: sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==}
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
||||
|
||||
'@types/katex@0.16.7':
|
||||
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
||||
@@ -5439,14 +5426,14 @@ packages:
|
||||
'@types/mime@1.3.5':
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
|
||||
'@types/ms@0.7.34':
|
||||
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
|
||||
'@types/ms@2.1.0':
|
||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||
|
||||
'@types/mysql@2.15.26':
|
||||
resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==}
|
||||
|
||||
'@types/node-fetch@2.6.12':
|
||||
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
|
||||
'@types/node-fetch@2.6.13':
|
||||
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
|
||||
|
||||
'@types/node@18.19.120':
|
||||
resolution: {integrity: sha512-WtCGHFXnVI8WHLxDAt5TbnCM4eSE+nI0QN2NJtwzcgMhht2eNz6V9evJrk+lwC8bCY8OWV5Ym8Jz7ZEyGnKnMA==}
|
||||
@@ -6352,8 +6339,8 @@ packages:
|
||||
caniuse-lite@1.0.30001723:
|
||||
resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==}
|
||||
|
||||
caniuse-lite@1.0.30001727:
|
||||
resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==}
|
||||
caniuse-lite@1.0.30001733:
|
||||
resolution: {integrity: sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q==}
|
||||
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
@@ -7245,8 +7232,8 @@ packages:
|
||||
electron-to-chromium@1.5.167:
|
||||
resolution: {integrity: sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==}
|
||||
|
||||
electron-to-chromium@1.5.192:
|
||||
resolution: {integrity: sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==}
|
||||
electron-to-chromium@1.5.199:
|
||||
resolution: {integrity: sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==}
|
||||
|
||||
electron-to-chromium@1.5.33:
|
||||
resolution: {integrity: sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==}
|
||||
@@ -7281,8 +7268,8 @@ packages:
|
||||
resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
enhanced-resolve@5.18.2:
|
||||
resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==}
|
||||
enhanced-resolve@5.18.3:
|
||||
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
entities@4.5.0:
|
||||
@@ -7651,9 +7638,6 @@ packages:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
eventemitter3@3.1.2:
|
||||
resolution: {integrity: sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==}
|
||||
|
||||
eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
|
||||
@@ -8515,10 +8499,6 @@ packages:
|
||||
is-ssh@1.4.1:
|
||||
resolution: {integrity: sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==}
|
||||
|
||||
is-stream@1.1.0:
|
||||
resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-stream@2.0.1:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -10479,7 +10459,7 @@ packages:
|
||||
|
||||
puppeteer@19.11.1:
|
||||
resolution: {integrity: sha512-39olGaX2djYUdhaQQHDZ0T0GwEp+5f9UB9HmEP0qHfdQHIq0xGQZuAZ5TLnJIc/88SrPLpEflPC+xUqOTv3c5g==}
|
||||
deprecated: < 22.8.2 is no longer supported
|
||||
deprecated: < 24.9.0 is no longer supported
|
||||
|
||||
pure-rand@6.0.4:
|
||||
resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==}
|
||||
@@ -12376,7 +12356,7 @@ snapshots:
|
||||
'@anthropic-ai/sdk@0.39.0':
|
||||
dependencies:
|
||||
'@types/node': 18.19.120
|
||||
'@types/node-fetch': 2.6.12
|
||||
'@types/node-fetch': 2.6.13
|
||||
abort-controller: 3.0.0
|
||||
agentkeepalive: 4.6.0
|
||||
form-data-encoder: 1.7.2
|
||||
@@ -14333,6 +14313,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/core@1.7.3':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/dom@1.6.11':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.6.8
|
||||
@@ -14348,6 +14332,11 @@ snapshots:
|
||||
'@floating-ui/core': 1.7.2
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/dom@1.7.3':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.3
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/react-dom@1.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.6.3
|
||||
@@ -14366,6 +14355,12 @@ snapshots:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
'@floating-ui/react-dom@2.1.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.3
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
'@floating-ui/react@0.19.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@floating-ui/react-dom': 1.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -14665,7 +14660,7 @@ snapshots:
|
||||
'@jest/console@29.7.0':
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 20.11.29
|
||||
'@types/node': 20.14.8
|
||||
chalk: 4.1.2
|
||||
jest-message-util: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
@@ -14750,7 +14745,7 @@ snapshots:
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
'@types/node': 20.11.29
|
||||
'@types/node': 20.14.8
|
||||
chalk: 4.1.2
|
||||
collect-v8-coverage: 1.0.2
|
||||
exit: 0.1.2
|
||||
@@ -15046,8 +15041,8 @@ snapshots:
|
||||
'@mui/base@5.0.0-beta.40(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.2
|
||||
'@floating-ui/react-dom': 2.1.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@mui/types': 7.4.4(@types/react@18.2.79)
|
||||
'@floating-ui/react-dom': 2.1.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@mui/types': 7.4.5(@types/react@18.2.79)
|
||||
'@mui/utils': 5.17.1(@types/react@18.2.79)(react@18.2.0)
|
||||
'@popperjs/core': 2.11.8
|
||||
clsx: 2.1.1
|
||||
@@ -15065,7 +15060,7 @@ snapshots:
|
||||
'@mui/base': 5.0.0-beta.40(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@mui/core-downloads-tracker': 5.18.0
|
||||
'@mui/system': 5.16.7(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react@18.2.0)
|
||||
'@mui/types': 7.4.4(@types/react@18.2.79)
|
||||
'@mui/types': 7.4.5(@types/react@18.2.79)
|
||||
'@mui/utils': 5.17.1(@types/react@18.2.79)(react@18.2.0)
|
||||
'@types/react-transition-group': 4.4.12(@types/react@18.2.79)
|
||||
clsx: 2.1.1
|
||||
@@ -15106,7 +15101,7 @@ snapshots:
|
||||
'@babel/runtime': 7.28.2
|
||||
'@mui/private-theming': 5.17.1(@types/react@18.2.79)(react@18.2.0)
|
||||
'@mui/styled-engine': 5.18.0(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0)
|
||||
'@mui/types': 7.4.4(@types/react@18.2.79)
|
||||
'@mui/types': 7.4.5(@types/react@18.2.79)
|
||||
'@mui/utils': 5.17.1(@types/react@18.2.79)(react@18.2.0)
|
||||
clsx: 2.1.1
|
||||
csstype: 3.1.3
|
||||
@@ -15125,7 +15120,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.79
|
||||
|
||||
'@mui/types@7.4.4(@types/react@18.2.79)':
|
||||
'@mui/types@7.4.5(@types/react@18.2.79)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.2
|
||||
optionalDependencies:
|
||||
@@ -17155,19 +17150,15 @@ snapshots:
|
||||
dependencies:
|
||||
'@sinonjs/commons': 3.0.1
|
||||
|
||||
'@slack/logger@3.0.0':
|
||||
dependencies:
|
||||
'@types/node': 20.11.29
|
||||
|
||||
'@slack/logger@4.0.0':
|
||||
dependencies:
|
||||
'@types/node': 20.11.29
|
||||
|
||||
'@slack/oauth@2.6.3':
|
||||
'@slack/oauth@3.0.3':
|
||||
dependencies:
|
||||
'@slack/logger': 3.0.0
|
||||
'@slack/web-api': 6.13.0
|
||||
'@types/jsonwebtoken': 8.5.9
|
||||
'@slack/logger': 4.0.0
|
||||
'@slack/web-api': 7.9.3
|
||||
'@types/jsonwebtoken': 9.0.10
|
||||
'@types/node': 20.11.29
|
||||
jsonwebtoken: 9.0.2
|
||||
lodash.isstring: 4.0.1
|
||||
@@ -17176,22 +17167,6 @@ snapshots:
|
||||
|
||||
'@slack/types@2.15.0': {}
|
||||
|
||||
'@slack/web-api@6.13.0':
|
||||
dependencies:
|
||||
'@slack/logger': 3.0.0
|
||||
'@slack/types': 2.15.0
|
||||
'@types/is-stream': 1.1.0
|
||||
'@types/node': 20.11.29
|
||||
axios: 1.8.2
|
||||
eventemitter3: 3.1.2
|
||||
form-data: 2.5.3
|
||||
is-electron: 2.2.2
|
||||
is-stream: 1.1.0
|
||||
p-queue: 6.6.2
|
||||
p-retry: 4.6.2
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
'@slack/web-api@7.9.3':
|
||||
dependencies:
|
||||
'@slack/logger': 4.0.0
|
||||
@@ -18075,7 +18050,7 @@ snapshots:
|
||||
|
||||
'@types/debug@4.1.8':
|
||||
dependencies:
|
||||
'@types/ms': 0.7.34
|
||||
'@types/ms': 2.1.0
|
||||
|
||||
'@types/deep-eql@4.0.2': {}
|
||||
|
||||
@@ -18114,7 +18089,7 @@ snapshots:
|
||||
|
||||
'@types/express-serve-static-core@4.19.6':
|
||||
dependencies:
|
||||
'@types/node': 20.11.29
|
||||
'@types/node': 20.14.8
|
||||
'@types/qs': 6.14.0
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 0.17.5
|
||||
@@ -18141,7 +18116,7 @@ snapshots:
|
||||
|
||||
'@types/graceful-fs@4.1.9':
|
||||
dependencies:
|
||||
'@types/node': 20.11.29
|
||||
'@types/node': 20.14.8
|
||||
|
||||
'@types/hammerjs@2.0.46': {}
|
||||
|
||||
@@ -18151,10 +18126,6 @@ snapshots:
|
||||
|
||||
'@types/http-errors@2.0.5': {}
|
||||
|
||||
'@types/is-stream@1.1.0':
|
||||
dependencies:
|
||||
'@types/node': 20.11.29
|
||||
|
||||
'@types/istanbul-lib-coverage@2.0.6': {}
|
||||
|
||||
'@types/istanbul-lib-report@3.0.3':
|
||||
@@ -18180,8 +18151,9 @@ snapshots:
|
||||
|
||||
'@types/json5@0.0.29': {}
|
||||
|
||||
'@types/jsonwebtoken@8.5.9':
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
'@types/node': 20.11.29
|
||||
|
||||
'@types/katex@0.16.7': {}
|
||||
@@ -18194,16 +18166,16 @@ snapshots:
|
||||
|
||||
'@types/mime@1.3.5': {}
|
||||
|
||||
'@types/ms@0.7.34': {}
|
||||
'@types/ms@2.1.0': {}
|
||||
|
||||
'@types/mysql@2.15.26':
|
||||
dependencies:
|
||||
'@types/node': 20.11.29
|
||||
|
||||
'@types/node-fetch@2.6.12':
|
||||
'@types/node-fetch@2.6.13':
|
||||
dependencies:
|
||||
'@types/node': 20.11.29
|
||||
form-data: 4.0.2
|
||||
form-data: 4.0.4
|
||||
|
||||
'@types/node@18.19.120':
|
||||
dependencies:
|
||||
@@ -19285,8 +19257,8 @@ snapshots:
|
||||
|
||||
browserslist@4.25.1:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001727
|
||||
electron-to-chromium: 1.5.192
|
||||
caniuse-lite: 1.0.30001733
|
||||
electron-to-chromium: 1.5.199
|
||||
node-releases: 2.0.19
|
||||
update-browserslist-db: 1.1.3(browserslist@4.25.1)
|
||||
|
||||
@@ -19385,7 +19357,7 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001723: {}
|
||||
|
||||
caniuse-lite@1.0.30001727: {}
|
||||
caniuse-lite@1.0.30001733: {}
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
@@ -20329,7 +20301,7 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.5.167: {}
|
||||
|
||||
electron-to-chromium@1.5.192: {}
|
||||
electron-to-chromium@1.5.199: {}
|
||||
|
||||
electron-to-chromium@1.5.33: {}
|
||||
|
||||
@@ -20359,7 +20331,7 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.2.1
|
||||
|
||||
enhanced-resolve@5.18.2:
|
||||
enhanced-resolve@5.18.3:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.2.2
|
||||
@@ -20976,8 +20948,6 @@ snapshots:
|
||||
|
||||
event-target-shim@5.0.1: {}
|
||||
|
||||
eventemitter3@3.1.2: {}
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
|
||||
eventemitter3@5.0.1: {}
|
||||
@@ -21942,8 +21912,6 @@ snapshots:
|
||||
dependencies:
|
||||
protocols: 2.0.2
|
||||
|
||||
is-stream@1.1.0: {}
|
||||
|
||||
is-stream@2.0.1: {}
|
||||
|
||||
is-stream@3.0.0: {}
|
||||
@@ -22076,7 +22044,7 @@ snapshots:
|
||||
'@jest/expect': 29.7.0
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 20.11.29
|
||||
'@types/node': 20.14.8
|
||||
chalk: 4.1.2
|
||||
co: 4.6.0
|
||||
dedent: 1.5.1(babel-plugin-macros@3.1.0)
|
||||
@@ -22236,7 +22204,7 @@ snapshots:
|
||||
'@jest/environment': 29.7.0
|
||||
'@jest/fake-timers': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 20.11.29
|
||||
'@types/node': 20.14.8
|
||||
jest-mock: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
|
||||
@@ -22246,7 +22214,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/graceful-fs': 4.1.9
|
||||
'@types/node': 20.11.29
|
||||
'@types/node': 20.14.8
|
||||
anymatch: 3.1.3
|
||||
fb-watchman: 2.0.2
|
||||
graceful-fs: 4.2.11
|
||||
@@ -22320,7 +22288,7 @@ snapshots:
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 20.11.29
|
||||
'@types/node': 20.14.8
|
||||
chalk: 4.1.2
|
||||
emittery: 0.13.1
|
||||
graceful-fs: 4.2.11
|
||||
@@ -22348,7 +22316,7 @@ snapshots:
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 20.11.29
|
||||
'@types/node': 20.14.8
|
||||
chalk: 4.1.2
|
||||
cjs-module-lexer: 1.4.1
|
||||
collect-v8-coverage: 1.0.2
|
||||
@@ -22413,7 +22381,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 20.11.29
|
||||
'@types/node': 20.14.8
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
emittery: 0.13.1
|
||||
@@ -22428,7 +22396,7 @@ snapshots:
|
||||
|
||||
jest-worker@29.7.0:
|
||||
dependencies:
|
||||
'@types/node': 20.11.29
|
||||
'@types/node': 20.14.8
|
||||
jest-util: 29.7.0
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
@@ -22674,40 +22642,13 @@ snapshots:
|
||||
- openai
|
||||
- ws
|
||||
|
||||
langchain@0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.8.2)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62)):
|
||||
dependencies:
|
||||
'@langchain/core': 0.3.58(openai@4.104.0(zod@3.25.62))
|
||||
'@langchain/openai': 0.5.13(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))
|
||||
'@langchain/textsplitters': 0.1.0(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))
|
||||
js-tiktoken: 1.0.15
|
||||
js-yaml: 4.1.0
|
||||
jsonpointer: 5.0.1
|
||||
langsmith: 0.3.30(openai@4.104.0(zod@3.25.62))
|
||||
openapi-types: 12.1.3
|
||||
p-retry: 4.6.2
|
||||
uuid: 10.0.0
|
||||
yaml: 2.5.1
|
||||
zod: 3.25.62
|
||||
optionalDependencies:
|
||||
'@langchain/anthropic': 0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))
|
||||
'@langchain/aws': 0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))
|
||||
'@langchain/google-genai': 0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))
|
||||
'@langchain/google-vertexai': 0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62)
|
||||
axios: 1.8.2
|
||||
cheerio: 1.0.0
|
||||
handlebars: 4.7.8
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- openai
|
||||
- ws
|
||||
|
||||
langfuse-core@3.38.4:
|
||||
dependencies:
|
||||
mustache: 4.2.0
|
||||
|
||||
langfuse-langchain@3.38.4(langchain@0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.8.2)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62))):
|
||||
langfuse-langchain@3.38.4(langchain@0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.11.0)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62))):
|
||||
dependencies:
|
||||
langchain: 0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.8.2)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62))
|
||||
langchain: 0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.11.0)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62))
|
||||
langfuse: 3.38.4
|
||||
langfuse-core: 3.38.4
|
||||
|
||||
@@ -23740,7 +23681,7 @@ snapshots:
|
||||
openai@4.104.0(zod@3.25.32):
|
||||
dependencies:
|
||||
'@types/node': 18.19.120
|
||||
'@types/node-fetch': 2.6.12
|
||||
'@types/node-fetch': 2.6.13
|
||||
abort-controller: 3.0.0
|
||||
agentkeepalive: 4.6.0
|
||||
form-data-encoder: 1.7.2
|
||||
@@ -23754,7 +23695,7 @@ snapshots:
|
||||
openai@4.104.0(zod@3.25.62):
|
||||
dependencies:
|
||||
'@types/node': 18.19.120
|
||||
'@types/node-fetch': 2.6.12
|
||||
'@types/node-fetch': 2.6.13
|
||||
abort-controller: 3.0.0
|
||||
agentkeepalive: 4.6.0
|
||||
form-data-encoder: 1.7.2
|
||||
@@ -26218,7 +26159,7 @@ snapshots:
|
||||
acorn: 8.15.0
|
||||
browserslist: 4.25.1
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.18.2
|
||||
enhanced-resolve: 5.18.3
|
||||
es-module-lexer: 1.7.0
|
||||
eslint-scope: 5.1.1
|
||||
events: 3.3.0
|
||||
|
||||
+2
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "3.95.2",
|
||||
"version": "3.98.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -9,7 +9,7 @@
|
||||
"scripts": {
|
||||
"build": "INLINE_RUNTIME_CHUNK=false dotenv -e ../.env -- next build",
|
||||
"dev": "dotenv -e ../.env -- next dev",
|
||||
"dev:https": "dotenv -e ../.env -- next dev --experimental-https --experimental-https-key ./localhost+1-key.pem --experimental-https-cert ./localhost+1.pem",
|
||||
"dev:http": "dotenv -e ../.env -- next dev --experimental-https --experimental-https-key ./localhost+1-key.pem --experimental-https-cert ./localhost+1.pem",
|
||||
"lint": "dotenv -e ../.env -- next lint --max-warnings 0",
|
||||
"lint:fix": "dotenv -e ../.env -- next lint --fix",
|
||||
"clean": "rm -rf node_modules",
|
||||
@@ -84,8 +84,6 @@
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@sentry/nextjs": "^8.52.0",
|
||||
"@slack/oauth": "^2.6.0",
|
||||
"@slack/web-api": "^7.0.0",
|
||||
"@t3-oss/env-nextjs": "^0.11.1",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
|
||||
@@ -79,6 +79,52 @@ paths:
|
||||
schema: {}
|
||||
security:
|
||||
- BasicAuth: []
|
||||
post:
|
||||
description: Create an annotation queue
|
||||
operationId: annotationQueues_createQueue
|
||||
tags:
|
||||
- AnnotationQueues
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AnnotationQueue'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security:
|
||||
- BasicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateAnnotationQueueRequest'
|
||||
/api/public/annotation-queues/{queueId}:
|
||||
get:
|
||||
description: Get an annotation queue by ID
|
||||
@@ -3254,7 +3300,7 @@ paths:
|
||||
parameters:
|
||||
- name: filter
|
||||
in: query
|
||||
description: Filter expression (e.g. userName eq 'value')
|
||||
description: Filter expression (e.g. userName eq "value")
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -4459,6 +4505,22 @@ components:
|
||||
required:
|
||||
- data
|
||||
- meta
|
||||
CreateAnnotationQueueRequest:
|
||||
title: CreateAnnotationQueueRequest
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
nullable: true
|
||||
scoreConfigIds:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- scoreConfigIds
|
||||
CreateAnnotationQueueItemRequest:
|
||||
title: CreateAnnotationQueueItemRequest
|
||||
type: object
|
||||
@@ -5908,6 +5970,13 @@ components:
|
||||
- SPAN
|
||||
- GENERATION
|
||||
- EVENT
|
||||
- AGENT
|
||||
- TOOL
|
||||
- CHAIN
|
||||
- RETRIEVER
|
||||
- EVALUATOR
|
||||
- EMBEDDING
|
||||
- GUARDRAIL
|
||||
IngestionUsage:
|
||||
title: IngestionUsage
|
||||
oneOf:
|
||||
|
||||
@@ -78,6 +78,39 @@
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Create Queue",
|
||||
"request": {
|
||||
"description": "Create an annotation queue",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/annotation-queues",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"annotation-queues"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"example\",\n \"description\": \"example\",\n \"scoreConfigIds\": [\n \"example\"\n ]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get Queue",
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
CreateAnnotationQueueItemResponse,
|
||||
UpdateAnnotationQueueItemResponse,
|
||||
DeleteAnnotationQueueItemResponse,
|
||||
CreateAnnotationQueueResponse,
|
||||
} from "@/src/features/public-api/types/annotation-queues";
|
||||
import {
|
||||
AnnotationQueueObjectType,
|
||||
@@ -173,6 +174,120 @@ describe("Annotation Queues API Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /annotation-queues", () => {
|
||||
it("should create a new annotation queue", async () => {
|
||||
const scoreConfig = await prisma.scoreConfig.create({
|
||||
data: {
|
||||
name: "Test Score Config",
|
||||
description: "Test Score Config Description",
|
||||
projectId,
|
||||
dataType: "NUMERIC",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
CreateAnnotationQueueResponse,
|
||||
"POST",
|
||||
"/api/public/annotation-queues",
|
||||
{
|
||||
name: "Test Queue",
|
||||
description: "Test Queue Description",
|
||||
scoreConfigIds: [scoreConfig.id],
|
||||
},
|
||||
auth,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.id).toBeDefined();
|
||||
expect(response.body.name).toBe("Test Queue");
|
||||
expect(response.body.description).toBe("Test Queue Description");
|
||||
expect(response.body.scoreConfigIds).toEqual([scoreConfig.id]);
|
||||
});
|
||||
|
||||
it("should return 400 if the queue name already exists", async () => {
|
||||
const response = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/annotation-queues",
|
||||
{
|
||||
name: "Test Queue",
|
||||
description: "Test Queue Description",
|
||||
scoreConfigIds: [],
|
||||
},
|
||||
auth,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it("should return 400 if no score config IDs are provided", async () => {
|
||||
const response = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/annotation-queues",
|
||||
{
|
||||
name: "No configs queue",
|
||||
description: "Test Queue Description",
|
||||
scoreConfigIds: [],
|
||||
},
|
||||
auth,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it("should return 400 if the score config IDs are invalid", async () => {
|
||||
const response = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/annotation-queues",
|
||||
{
|
||||
name: "Invalid configs queue",
|
||||
description: "Test Queue Description",
|
||||
scoreConfigIds: ["invalid-score-config-id"],
|
||||
},
|
||||
auth,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it("should return 405 if the user is on the Hobby plan and has reached the maximum number of annotation queues", async () => {
|
||||
const { auth: hobbyPlanAuth, projectId: hobbyProjectId } =
|
||||
await createOrgProjectAndApiKey({
|
||||
plan: "Hobby",
|
||||
});
|
||||
|
||||
const config = await prisma.scoreConfig.create({
|
||||
data: {
|
||||
name: "Test Score Config",
|
||||
description: "Test Score Config Description",
|
||||
projectId: hobbyProjectId,
|
||||
dataType: "NUMERIC",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.annotationQueue.create({
|
||||
data: {
|
||||
name: "First queue",
|
||||
description: "First queue description",
|
||||
scoreConfigIds: [config.id],
|
||||
projectId: hobbyProjectId,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/annotation-queues",
|
||||
{
|
||||
name: "Hobby plan queue",
|
||||
description: "Test Queue Description",
|
||||
scoreConfigIds: [config.id],
|
||||
},
|
||||
hobbyPlanAuth,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(405);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /annotation-queues/:queueId", () => {
|
||||
it("should get a specific annotation queue", async () => {
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
|
||||
@@ -205,6 +205,162 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"agent",
|
||||
"AGENT",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "agent-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId: randomUUID(),
|
||||
startTime: new Date().toISOString(),
|
||||
endTime: new Date(Date.now() + 1000).toISOString(),
|
||||
name: "AI Agent",
|
||||
input: "Process user request",
|
||||
output: "Request processed successfully",
|
||||
model: "claude-3-haiku",
|
||||
modelParameters: { temperature: 0.7, max_tokens: 1000 },
|
||||
usage: {
|
||||
input: 150,
|
||||
output: 75,
|
||||
total: 225,
|
||||
unit: "TOKENS",
|
||||
inputCost: 0.0015,
|
||||
outputCost: 0.003,
|
||||
totalCost: 0.0045,
|
||||
},
|
||||
usageDetails: { input: 150, output: 75, total: 225 },
|
||||
costDetails: { input: 0.0015, output: 0.003, total: 0.0045 },
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"tool",
|
||||
"TOOL",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "tool-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId: randomUUID(),
|
||||
startTime: new Date().toISOString(),
|
||||
endTime: new Date(Date.now() + 2000).toISOString(),
|
||||
name: "Web Search Tool",
|
||||
input: "Search for current weather",
|
||||
output: "Weather data retrieved",
|
||||
model: "gpt-4o-mini",
|
||||
usage: {
|
||||
input: 50,
|
||||
output: 25,
|
||||
total: 75,
|
||||
unit: "TOKENS",
|
||||
inputCost: 0.0001,
|
||||
outputCost: 0.0002,
|
||||
totalCost: 0.0003,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"chain",
|
||||
"CHAIN",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "chain-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId: randomUUID(),
|
||||
startTime: new Date().toISOString(),
|
||||
endTime: new Date(Date.now() + 3000).toISOString(),
|
||||
name: "Processing Chain",
|
||||
input: "Multi-step task",
|
||||
output: "All steps completed",
|
||||
model: "gpt-4",
|
||||
usageDetails: { input: 800, output: 400, total: 1200 },
|
||||
costDetails: { input: 0.024, output: 0.048, total: 0.072 },
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"retriever",
|
||||
"RETRIEVER",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "retriever-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId: randomUUID(),
|
||||
startTime: new Date().toISOString(),
|
||||
endTime: new Date(Date.now() + 1500).toISOString(),
|
||||
name: "Document Retriever",
|
||||
input: "Query document database",
|
||||
output: "Retrieved 5 relevant documents",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"evaluator",
|
||||
"EVALUATOR",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "evaluator-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId: randomUUID(),
|
||||
startTime: new Date().toISOString(),
|
||||
endTime: new Date(Date.now() + 800).toISOString(),
|
||||
name: "Quality Evaluator",
|
||||
input: "Evaluate response quality",
|
||||
output: "Quality score: 0.85",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"embedding",
|
||||
"EMBEDDING",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "embedding-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId: randomUUID(),
|
||||
startTime: new Date().toISOString(),
|
||||
endTime: new Date(Date.now() + 500).toISOString(),
|
||||
name: "Text Embedding",
|
||||
input: "Text to embed",
|
||||
output: "Embedding vector generated",
|
||||
model: "text-embedding-ada-002",
|
||||
usage: {
|
||||
input: 20,
|
||||
output: 0,
|
||||
total: 20,
|
||||
unit: "TOKENS",
|
||||
totalCost: 0.00004,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"guardrail",
|
||||
"GUARDRAIL",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "guardrail-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId: randomUUID(),
|
||||
startTime: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
])(
|
||||
"should create observations via the ingestion API (%s)",
|
||||
async (_name: string, type: string, entity: any) => {
|
||||
|
||||
@@ -497,4 +497,137 @@ describe("/api/public/metrics API Endpoint", () => {
|
||||
expect(height).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("LFE-6148: Comprehensive filter validation", () => {
|
||||
it("should return 400 error for invalid array field filters", async () => {
|
||||
// Test using string type on array field (tags) - should return validation error
|
||||
const invalidStringTypeQuery = {
|
||||
view: "traces",
|
||||
dimensions: [{ field: "name" }],
|
||||
metrics: [{ measure: "count", aggregation: "count" }],
|
||||
filters: [
|
||||
{
|
||||
column: "tags",
|
||||
operator: "contains",
|
||||
value: "test-tag",
|
||||
type: "string", // Invalid: array fields require arrayOptions type
|
||||
},
|
||||
],
|
||||
timeDimension: {
|
||||
granularity: "day",
|
||||
},
|
||||
fromTimestamp: yesterday.toISOString(),
|
||||
toTimestamp: tomorrow.toISOString(),
|
||||
orderBy: null,
|
||||
};
|
||||
|
||||
// Make API call and expect 400 error
|
||||
const response = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/metrics?query=${encodeURIComponent(JSON.stringify(invalidStringTypeQuery))}`,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toMatchObject({
|
||||
error: "InvalidRequestError",
|
||||
message: expect.stringContaining(
|
||||
"Array fields require type 'arrayOptions', not 'string'",
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("should return 400 error for invalid metadata filters", async () => {
|
||||
// Test using wrong type for metadata field
|
||||
const invalidMetadataTypeQuery = {
|
||||
view: "traces",
|
||||
dimensions: [{ field: "name" }],
|
||||
metrics: [{ measure: "count", aggregation: "count" }],
|
||||
filters: [
|
||||
{
|
||||
column: "metadata",
|
||||
operator: "contains",
|
||||
value: "test-value",
|
||||
type: "string", // Invalid: metadata requires stringObject type
|
||||
},
|
||||
],
|
||||
timeDimension: null,
|
||||
fromTimestamp: yesterday.toISOString(),
|
||||
toTimestamp: tomorrow.toISOString(),
|
||||
orderBy: null,
|
||||
};
|
||||
|
||||
const response = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/metrics?query=${encodeURIComponent(JSON.stringify(invalidMetadataTypeQuery))}`,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toMatchObject({
|
||||
error: "InvalidRequestError",
|
||||
message: expect.stringContaining(
|
||||
"Metadata filters require type 'stringObject'",
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("should work correctly with proper array field filter configuration", async () => {
|
||||
// Setup test data with tags
|
||||
const taggedTraceId = randomUUID();
|
||||
await createTracesCh([
|
||||
createTrace({
|
||||
id: taggedTraceId,
|
||||
name: "tagged-trace",
|
||||
project_id: projectId,
|
||||
timestamp: now.getTime(),
|
||||
tags: ["test-tag", "another-tag"],
|
||||
metadata: { test: testMetadataValue },
|
||||
}),
|
||||
]);
|
||||
|
||||
// Test with correct arrayOptions filter
|
||||
const validQuery = {
|
||||
view: "traces",
|
||||
dimensions: [{ field: "name" }],
|
||||
metrics: [{ measure: "count", aggregation: "count" }],
|
||||
filters: [
|
||||
{
|
||||
column: "tags",
|
||||
operator: "any of", // Correct operator for array fields
|
||||
value: ["test-tag"],
|
||||
type: "arrayOptions", // Correct type for array fields
|
||||
},
|
||||
{
|
||||
column: "metadata",
|
||||
operator: "contains",
|
||||
key: "test",
|
||||
value: testMetadataValue,
|
||||
type: "stringObject",
|
||||
},
|
||||
],
|
||||
timeDimension: null,
|
||||
fromTimestamp: yesterday.toISOString(),
|
||||
toTimestamp: tomorrow.toISOString(),
|
||||
orderBy: null,
|
||||
};
|
||||
|
||||
// Make the API call
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
GetMetricsV1Response,
|
||||
"GET",
|
||||
`/api/public/metrics?query=${encodeURIComponent(JSON.stringify(validQuery))}`,
|
||||
);
|
||||
|
||||
// Should succeed and return data
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
expect(response.body.data.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify we got the tagged trace
|
||||
const taggedTraceResult = response.body.data.find(
|
||||
(row: any) => row.name === "tagged-trace",
|
||||
);
|
||||
expect(taggedTraceResult).toBeDefined();
|
||||
expect(taggedTraceResult.count_count).toBe("1");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,6 +45,41 @@ describe("/api/public/observations API Endpoint", () => {
|
||||
output: observation.output,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["AGENT", "agent-observation"],
|
||||
["TOOL", "tool-observation"],
|
||||
["CHAIN", "chain-observation"],
|
||||
["RETRIEVER", "retriever-observation"],
|
||||
["EVALUATOR", "evaluator-observation"],
|
||||
["EMBEDDING", "embedding-observation"],
|
||||
["GUARDRAIL", "guardrail-observation"],
|
||||
])("should GET observation with type %s", async (type, name) => {
|
||||
const observationId = uuidv4();
|
||||
const traceId = uuidv4();
|
||||
|
||||
const observation = createObservationObject({
|
||||
id: observationId,
|
||||
project_id: projectId,
|
||||
trace_id: traceId,
|
||||
type: type,
|
||||
name: name,
|
||||
});
|
||||
|
||||
await createObservationsInClickhouse([observation]);
|
||||
|
||||
const getEventRes = await makeZodVerifiedAPICall(
|
||||
GetObservationV1Response,
|
||||
"GET",
|
||||
"/api/public/observations/" + observationId,
|
||||
);
|
||||
expect(getEventRes.body).toMatchObject({
|
||||
id: observationId,
|
||||
traceId: traceId,
|
||||
type: type,
|
||||
name: name,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/public/observations", () => {
|
||||
|
||||
@@ -66,6 +66,98 @@ describe("/api/public/observations API Endpoint", () => {
|
||||
input: "User action recorded",
|
||||
metadata: { eventType: "click", target: "submit-button" },
|
||||
}),
|
||||
createObservation({
|
||||
id: randomUUID(),
|
||||
trace_id: traceId,
|
||||
project_id: projectId,
|
||||
name: "agent-observation",
|
||||
type: "AGENT",
|
||||
level: "DEFAULT",
|
||||
start_time: timestamp.getTime() + 2000,
|
||||
end_time: timestamp.getTime() + 2500,
|
||||
provided_model_name: "claude-3-haiku",
|
||||
provided_usage_details: { input: 100, output: 50, total: 150 },
|
||||
provided_cost_details: { input: 0.001, output: 0.002, total: 0.003 },
|
||||
}),
|
||||
createObservation({
|
||||
id: randomUUID(),
|
||||
trace_id: traceId,
|
||||
project_id: projectId,
|
||||
name: "tool-observation",
|
||||
type: "TOOL",
|
||||
level: "DEFAULT",
|
||||
start_time: timestamp.getTime() + 2500,
|
||||
end_time: timestamp.getTime() + 3000,
|
||||
input: "Search web for information",
|
||||
output: "Found relevant results",
|
||||
provided_model_name: "gpt-4o-mini",
|
||||
provided_usage_details: { input: 200, output: 100, total: 300 },
|
||||
provided_cost_details: { input: 0.002, output: 0.004, total: 0.006 },
|
||||
}),
|
||||
createObservation({
|
||||
id: randomUUID(),
|
||||
trace_id: traceId,
|
||||
project_id: projectId,
|
||||
name: "chain-observation",
|
||||
type: "CHAIN",
|
||||
level: "DEFAULT",
|
||||
start_time: timestamp.getTime() + 3000,
|
||||
end_time: timestamp.getTime() + 3500,
|
||||
input: "Process multi-step workflow",
|
||||
output: "Workflow completed",
|
||||
provided_model_name: "gpt-4",
|
||||
provided_usage_details: { input: 500, output: 300, total: 800 },
|
||||
provided_cost_details: { input: 0.015, output: 0.03, total: 0.045 },
|
||||
}),
|
||||
createObservation({
|
||||
id: randomUUID(),
|
||||
trace_id: traceId,
|
||||
project_id: projectId,
|
||||
name: "retriever-observation",
|
||||
type: "RETRIEVER",
|
||||
level: "DEFAULT",
|
||||
start_time: timestamp.getTime() + 3500,
|
||||
end_time: timestamp.getTime() + 4000,
|
||||
input: "Query document database",
|
||||
output: "Retrieved 5 relevant documents",
|
||||
}),
|
||||
createObservation({
|
||||
id: randomUUID(),
|
||||
trace_id: traceId,
|
||||
project_id: projectId,
|
||||
name: "evaluator-observation",
|
||||
type: "EVALUATOR",
|
||||
level: "DEFAULT",
|
||||
start_time: timestamp.getTime() + 4000,
|
||||
input: null,
|
||||
output: null,
|
||||
end_time: null,
|
||||
}),
|
||||
createObservation({
|
||||
id: randomUUID(),
|
||||
trace_id: traceId,
|
||||
project_id: projectId,
|
||||
name: "embedding-observation",
|
||||
type: "EMBEDDING",
|
||||
level: "DEFAULT",
|
||||
start_time: timestamp.getTime() + 4500,
|
||||
end_time: timestamp.getTime() + 4750,
|
||||
input: "Text to embed",
|
||||
output: "Vector embedding generated",
|
||||
provided_model_name: "text-embedding-ada-002",
|
||||
provided_usage_details: { input: 10, output: 0, total: 10 },
|
||||
provided_cost_details: { input: 0.0001, output: 0, total: 0.0001 },
|
||||
}),
|
||||
createObservation({
|
||||
id: randomUUID(),
|
||||
trace_id: traceId,
|
||||
project_id: projectId,
|
||||
name: "guardrail-observation",
|
||||
type: "GUARDRAIL",
|
||||
level: "DEFAULT",
|
||||
start_time: timestamp.getTime() + 5000,
|
||||
provided_cost_details: { input: 0.0001, output: 0, total: 0.0001 },
|
||||
}),
|
||||
];
|
||||
|
||||
await createTracesCh([createdTrace]);
|
||||
@@ -80,14 +172,14 @@ describe("/api/public/observations API Endpoint", () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.meta).toBeDefined();
|
||||
expect(response.body.meta.totalItems).toBeGreaterThanOrEqual(3);
|
||||
expect(response.body.data.length).toBeGreaterThanOrEqual(3);
|
||||
expect(response.body.meta.totalItems).toBeGreaterThanOrEqual(10);
|
||||
expect(response.body.data.length).toBeGreaterThanOrEqual(10);
|
||||
|
||||
// Find our created observations in the response
|
||||
const createdObservations = response.body.data.filter(
|
||||
(obs) => obs.traceId === traceId,
|
||||
);
|
||||
expect(createdObservations.length).toBe(3);
|
||||
expect(createdObservations.length).toBe(10);
|
||||
|
||||
// Verify data structure and content
|
||||
const generationObs = createdObservations.find(
|
||||
@@ -118,6 +210,75 @@ describe("/api/public/observations API Endpoint", () => {
|
||||
eventType: "click",
|
||||
target: "submit-button",
|
||||
});
|
||||
|
||||
// Verify new observation types exist and have correct type
|
||||
const agentObs = createdObservations.find((obs) => obs.type === "AGENT");
|
||||
expect(agentObs).toBeDefined();
|
||||
expect(agentObs?.name).toBe("agent-observation");
|
||||
|
||||
const toolObs = createdObservations.find((obs) => obs.type === "TOOL");
|
||||
expect(toolObs).toBeDefined();
|
||||
expect(toolObs?.name).toBe("tool-observation");
|
||||
|
||||
const chainObs = createdObservations.find((obs) => obs.type === "CHAIN");
|
||||
expect(chainObs).toBeDefined();
|
||||
expect(chainObs?.name).toBe("chain-observation");
|
||||
|
||||
const retrieverObs = createdObservations.find(
|
||||
(obs) => obs.type === "RETRIEVER",
|
||||
);
|
||||
expect(retrieverObs).toBeDefined();
|
||||
expect(retrieverObs?.name).toBe("retriever-observation");
|
||||
|
||||
const evaluatorObs = createdObservations.find(
|
||||
(obs) => obs.type === "EVALUATOR",
|
||||
);
|
||||
expect(evaluatorObs).toBeDefined();
|
||||
expect(evaluatorObs?.name).toBe("evaluator-observation");
|
||||
// Test that input, output, and endTime can be null (optional fields)
|
||||
expect(evaluatorObs?.input).toBeNull();
|
||||
expect(evaluatorObs?.output).toBeNull();
|
||||
expect(evaluatorObs?.endTime).toBeNull();
|
||||
|
||||
const embeddingObs = createdObservations.find(
|
||||
(obs) => obs.type === "EMBEDDING",
|
||||
);
|
||||
expect(embeddingObs).toBeDefined();
|
||||
expect(embeddingObs?.name).toBe("embedding-observation");
|
||||
|
||||
const guardrailObs = createdObservations.find(
|
||||
(obs) => obs.type === "GUARDRAIL",
|
||||
);
|
||||
expect(guardrailObs).toBeDefined();
|
||||
expect(guardrailObs?.name).toBe("guardrail-observation");
|
||||
|
||||
// Verify new observation types support model and cost attributes
|
||||
// The key verification is that new observation types now have model and cost fields populated
|
||||
// (even if with default values from the factory, proving the schema changes work)
|
||||
expect(agentObs?.model).toBe("claude-3-haiku");
|
||||
expect(agentObs?.input).toBe("Hello World");
|
||||
expect(agentObs?.output).toBe("Hello John");
|
||||
// Verify that model/cost fields are present (core functionality test)
|
||||
expect(agentObs?.usageDetails).toBeDefined();
|
||||
expect(agentObs?.costDetails).toBeDefined();
|
||||
|
||||
expect(toolObs?.model).toBe("gpt-4o-mini");
|
||||
expect(toolObs?.input).toBe("Search web for information");
|
||||
expect(toolObs?.output).toBe("Found relevant results");
|
||||
expect(toolObs?.usageDetails).toBeDefined();
|
||||
expect(toolObs?.costDetails).toBeDefined();
|
||||
|
||||
expect(chainObs?.model).toBe("gpt-4");
|
||||
expect(chainObs?.input).toBe("Process multi-step workflow");
|
||||
expect(chainObs?.output).toBe("Workflow completed");
|
||||
expect(chainObs?.usageDetails).toBeDefined();
|
||||
expect(chainObs?.costDetails).toBeDefined();
|
||||
|
||||
expect(embeddingObs?.model).toBe("text-embedding-ada-002");
|
||||
expect(embeddingObs?.input).toBe("Text to embed");
|
||||
expect(embeddingObs?.output).toBe("Vector embedding generated");
|
||||
expect(embeddingObs?.usageDetails).toBeDefined();
|
||||
expect(embeddingObs?.costDetails).toBeDefined();
|
||||
}, 20_000);
|
||||
|
||||
it("should filter observations by level parameter", async () => {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
type PivotTableConfig,
|
||||
type DatabaseRow,
|
||||
type PivotTableRow,
|
||||
getNextSortState,
|
||||
} from "@/src/features/widgets/utils/pivot-table-utils";
|
||||
|
||||
describe("pivot-table-utils", () => {
|
||||
@@ -1014,4 +1015,113 @@ describe("pivot-table-utils", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("getNextSortState", () => {
|
||||
const defaultSort = { column: "avg_cost", order: "DESC" as const };
|
||||
|
||||
describe("no current sort", () => {
|
||||
test("returns DESC when no current sort", () => {
|
||||
const next = getNextSortState(null, null, "count");
|
||||
expect(next).toEqual({ column: "count", order: "DESC" });
|
||||
});
|
||||
|
||||
test("returns DESC when no current sort with default sort defined", () => {
|
||||
const next = getNextSortState(defaultSort, null, "count");
|
||||
expect(next).toEqual({ column: "count", order: "DESC" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("different column", () => {
|
||||
test("returns DESC when different column", () => {
|
||||
const next = getNextSortState(
|
||||
null,
|
||||
{ column: "metric", order: "ASC" },
|
||||
"count",
|
||||
);
|
||||
expect(next).toEqual({ column: "count", order: "DESC" });
|
||||
});
|
||||
|
||||
test("returns DESC when different column with default sort", () => {
|
||||
const next = getNextSortState(
|
||||
defaultSort,
|
||||
{ column: "metric", order: "ASC" },
|
||||
"count",
|
||||
);
|
||||
expect(next).toEqual({ column: "count", order: "DESC" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("non-default column cycling", () => {
|
||||
test("non-default column: DESC -> ASC", () => {
|
||||
const next = getNextSortState(
|
||||
defaultSort,
|
||||
{ column: "count", order: "DESC" },
|
||||
"count",
|
||||
);
|
||||
expect(next).toEqual({ column: "count", order: "ASC" });
|
||||
});
|
||||
|
||||
test("non-default column: ASC -> default sort", () => {
|
||||
const next = getNextSortState(
|
||||
defaultSort,
|
||||
{ column: "count", order: "ASC" },
|
||||
"count",
|
||||
);
|
||||
expect(next).toEqual(defaultSort);
|
||||
});
|
||||
|
||||
test("non-default column: ASC -> null when no default sort", () => {
|
||||
const next = getNextSortState(
|
||||
null,
|
||||
{ column: "count", order: "ASC" },
|
||||
"count",
|
||||
);
|
||||
expect(next).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("default column cycling", () => {
|
||||
test("default column: DESC -> ASC", () => {
|
||||
const next = getNextSortState(
|
||||
defaultSort,
|
||||
{ column: "avg_cost", order: "DESC" },
|
||||
"avg_cost",
|
||||
);
|
||||
expect(next).toEqual({ column: "avg_cost", order: "ASC" });
|
||||
});
|
||||
|
||||
test("default column: ASC -> DESC (infinite cycle)", () => {
|
||||
const next = getNextSortState(
|
||||
defaultSort,
|
||||
{ column: "avg_cost", order: "ASC" },
|
||||
"avg_cost",
|
||||
);
|
||||
expect(next).toEqual({ column: "avg_cost", order: "DESC" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
test("handles undefined default sort", () => {
|
||||
const next = getNextSortState(
|
||||
undefined as any,
|
||||
{ column: "count", order: "ASC" },
|
||||
"count",
|
||||
);
|
||||
expect(next).toBeNull();
|
||||
});
|
||||
|
||||
test("handles null default sort", () => {
|
||||
const next = getNextSortState(
|
||||
null,
|
||||
{ column: "count", order: "ASC" },
|
||||
"count",
|
||||
);
|
||||
expect(next).toBeNull();
|
||||
});
|
||||
|
||||
test("handles null current sort with defined default", () => {
|
||||
const next = getNextSortState(defaultSort, null, "count");
|
||||
expect(next).toEqual({ column: "count", order: "DESC" });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3304,5 +3304,48 @@ describe("OTel Resource Span Mapping", () => {
|
||||
expect(traceEvent).toBeDefined();
|
||||
expect(traceEvent.body.sessionId).toBe("session-id-123");
|
||||
});
|
||||
|
||||
it("should default to span-create for unknown observation type", async () => {
|
||||
const otelSpans = [
|
||||
{
|
||||
resource: { attributes: [] },
|
||||
scopeSpans: [
|
||||
{
|
||||
scope: { name: "test-scope" },
|
||||
spans: [
|
||||
{
|
||||
traceId: {
|
||||
data: [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
||||
],
|
||||
},
|
||||
spanId: { data: [1, 2, 3, 4, 5, 6, 7, 8] },
|
||||
name: "test-span",
|
||||
startTimeUnixNano: 1000000000,
|
||||
endTimeUnixNano: 2000000000,
|
||||
attributes: [
|
||||
{
|
||||
key: "langfuse.observation.type",
|
||||
value: { stringValue: "invalid_type" },
|
||||
},
|
||||
],
|
||||
status: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const events = await convertOtelSpanToIngestionEvent(
|
||||
otelSpans[0],
|
||||
new Set(),
|
||||
publicKey,
|
||||
);
|
||||
const spanEvents = events.filter((e) => e.type === "span-create");
|
||||
|
||||
expect(spanEvents.length).toBe(1);
|
||||
expect(spanEvents[0].body.name).toBe("test-span");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
IngestionQueue,
|
||||
logger,
|
||||
QueueName,
|
||||
TraceUpsertQueue,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { type z } from "zod/v4";
|
||||
|
||||
@@ -101,7 +102,10 @@ export const pruneDatabase = async () => {
|
||||
};
|
||||
export const getQueues = () => {
|
||||
const queues: string[] = Object.values(QueueName);
|
||||
queues.push(...IngestionQueue.getShardNames());
|
||||
queues.push(
|
||||
...IngestionQueue.getShardNames(),
|
||||
...TraceUpsertQueue.getShardNames(),
|
||||
);
|
||||
|
||||
const listOfQueuesToIgnore = [
|
||||
QueueName.DataRetentionQueue,
|
||||
@@ -117,7 +121,14 @@ export const getQueues = () => {
|
||||
.map((queueName) =>
|
||||
queueName.startsWith(QueueName.IngestionQueue)
|
||||
? IngestionQueue.getInstance({ shardName: queueName })
|
||||
: getQueue(queueName as Exclude<QueueName, QueueName.IngestionQueue>),
|
||||
: queueName.startsWith(QueueName.TraceUpsert)
|
||||
? TraceUpsertQueue.getInstance({ shardName: queueName })
|
||||
: getQueue(
|
||||
queueName as Exclude<
|
||||
QueueName,
|
||||
QueueName.IngestionQueue | QueueName.TraceUpsert
|
||||
>,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ import {
|
||||
TestTubeDiagonal,
|
||||
Clock,
|
||||
Bot,
|
||||
Wrench,
|
||||
Link,
|
||||
Search,
|
||||
Layers3,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { type ObservationType } from "@langfuse/shared";
|
||||
@@ -38,6 +43,12 @@ const iconMap = {
|
||||
GENERATION: Fan,
|
||||
EVENT: CircleDot,
|
||||
SPAN: MoveHorizontal,
|
||||
AGENT: Bot,
|
||||
TOOL: Wrench,
|
||||
CHAIN: Link,
|
||||
RETRIEVER: Search,
|
||||
EMBEDDING: Layers3,
|
||||
GUARDRAIL: ShieldCheck,
|
||||
SESSION: Clock,
|
||||
USER: User,
|
||||
QUEUE_ITEM: ClipboardPen,
|
||||
@@ -57,6 +68,12 @@ const iconVariants = cva(cn("h-4 w-4"), {
|
||||
GENERATION: "text-muted-magenta",
|
||||
EVENT: "text-muted-green",
|
||||
SPAN: "text-muted-blue",
|
||||
AGENT: "text-purple-600",
|
||||
TOOL: "text-orange-600",
|
||||
CHAIN: "text-indigo-600",
|
||||
RETRIEVER: "text-teal-600",
|
||||
EMBEDDING: "text-amber-600",
|
||||
GUARDRAIL: "text-red-600",
|
||||
SESSION: "text-primary-accent",
|
||||
USER: "text-primary-accent",
|
||||
QUEUE_ITEM: "text-primary-accent",
|
||||
|
||||
@@ -15,19 +15,27 @@ import { CreateLLMApiKeyDialog } from "@/src/features/public-api/components/Crea
|
||||
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import {
|
||||
type LLMAdapter,
|
||||
type JSONObject,
|
||||
JSONObjectSchema,
|
||||
LLMAdapter,
|
||||
type supportedModels,
|
||||
type UIModelParams,
|
||||
} from "@langfuse/shared";
|
||||
import { Settings2 } from "lucide-react";
|
||||
import { InfoIcon, Settings2 } from "lucide-react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/src/components/ui/popover";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/src/components/ui/tooltip";
|
||||
|
||||
import { LLMApiKeyComponent } from "./LLMApiKeyComponent";
|
||||
import { FormDescription } from "@/src/components/ui/form";
|
||||
import { CodeMirrorEditor } from "../editor";
|
||||
|
||||
export type ModelParamsContext = {
|
||||
modelParams: UIModelParams;
|
||||
@@ -61,6 +69,9 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
|
||||
const [modelSettingsOpen, setModelSettingsOpen] = useState(false);
|
||||
const [modelSettingsUsed, setModelSettingsUsed] = useState(false);
|
||||
|
||||
const [createLlmApiKeyDialogOpen, setCreateLlmApiKeyDialogOpen] =
|
||||
useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const hasEnabledModelSetting = Object.keys(modelParams).some(
|
||||
(key) =>
|
||||
@@ -88,11 +99,19 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs">No LLM API key set in project. </p>
|
||||
<CreateLLMApiKeyDialog />
|
||||
<CreateLLMApiKeyDialog
|
||||
open={createLlmApiKeyDialogOpen}
|
||||
setOpen={setCreateLlmApiKeyDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isProviderOptionsSupported = ![
|
||||
LLMAdapter.GoogleAIStudio,
|
||||
LLMAdapter.VertexAI,
|
||||
].includes(modelParams.adapter.value);
|
||||
|
||||
// Settings button component for reuse
|
||||
const SettingsButton = (
|
||||
<Popover open={modelSettingsOpen} onOpenChange={setModelSettingsOpen}>
|
||||
@@ -160,6 +179,15 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
|
||||
tooltip="An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both."
|
||||
updateModelParam={updateModelParamValue}
|
||||
/>
|
||||
{isProviderOptionsSupported ? (
|
||||
<ProviderOptionsInput
|
||||
value={modelParams.providerOptions.value}
|
||||
formDisabled={formDisabled}
|
||||
enabled={modelParams.providerOptions.enabled}
|
||||
setModelParamEnabled={setModelParamEnabled}
|
||||
updateModelParam={updateModelParamValue}
|
||||
/>
|
||||
) : null}
|
||||
<LLMApiKeyComponent {...{ projectId, modelParams }} />
|
||||
</div>
|
||||
</PopoverContent>
|
||||
@@ -204,7 +232,10 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectSeparator />
|
||||
<CreateLLMApiKeyDialog />
|
||||
<CreateLLMApiKeyDialog
|
||||
open={createLlmApiKeyDialogOpen}
|
||||
setOpen={setCreateLlmApiKeyDialogOpen}
|
||||
/>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{modelParamsDescription ? (
|
||||
@@ -264,21 +295,6 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
|
||||
layout="vertical"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{modelParams.model.value?.startsWith("o1-") ? (
|
||||
<p className="mt-1 text-xs text-dark-yellow">
|
||||
For {modelParams.model.value}, the system message and the
|
||||
temperature, max_tokens and top_p setting are not supported while it
|
||||
is in beta.{" "}
|
||||
<a
|
||||
href="https://platform.openai.com/docs/guides/reasoning/beta-limitations"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
More info ↗
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -304,6 +320,9 @@ const ModelParamsSelect = ({
|
||||
modelParamsDescription,
|
||||
layout = "vertical",
|
||||
}: ModelParamsSelectProps) => {
|
||||
const [createLlmApiKeyDialogOpen, setCreateLlmApiKeyDialogOpen] =
|
||||
useState(false);
|
||||
|
||||
// Compact layout - simplified, space-efficient (no individual labels)
|
||||
if (layout === "compact") {
|
||||
return (
|
||||
@@ -328,7 +347,10 @@ const ModelParamsSelect = ({
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectSeparator />
|
||||
<CreateLLMApiKeyDialog />
|
||||
<CreateLLMApiKeyDialog
|
||||
open={createLlmApiKeyDialogOpen}
|
||||
setOpen={setCreateLlmApiKeyDialogOpen}
|
||||
/>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{modelParamsDescription ? (
|
||||
@@ -374,7 +396,10 @@ const ModelParamsSelect = ({
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectSeparator />
|
||||
<CreateLLMApiKeyDialog />
|
||||
<CreateLLMApiKeyDialog
|
||||
open={createLlmApiKeyDialogOpen}
|
||||
setOpen={setCreateLlmApiKeyDialogOpen}
|
||||
/>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{modelParamsDescription ? (
|
||||
@@ -466,3 +491,92 @@ const ModelParamsSlider = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ProviderOptionsInputProps = {
|
||||
value: JSONObject | undefined;
|
||||
updateModelParam: ModelParamsContext["updateModelParamValue"];
|
||||
setModelParamEnabled: ModelParamsContext["setModelParamEnabled"];
|
||||
enabled: boolean;
|
||||
formDisabled: boolean;
|
||||
};
|
||||
const ProviderOptionsInput = ({
|
||||
value,
|
||||
updateModelParam,
|
||||
setModelParamEnabled,
|
||||
enabled,
|
||||
formDisabled,
|
||||
}: ProviderOptionsInputProps) => {
|
||||
const [inputValue, setInputValue] = useState<string>(
|
||||
value ? JSON.stringify(value, null, 2) : "{}",
|
||||
);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="space-y-3"
|
||||
title="Additional options to pass to the invocation. Please check your provider's API reference for supported values."
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<div className="flex-1 flex-row space-x-1">
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-semibold",
|
||||
(!enabled || formDisabled) && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
Additional options
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="size-3 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[200px] p-2">
|
||||
Additional options to pass to the invocation. Please check your
|
||||
provider's API reference for supported values.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex flex-row space-x-3">
|
||||
{setModelParamEnabled ? (
|
||||
<Switch
|
||||
title={`Control sending the additional options parameter`}
|
||||
disabled={formDisabled}
|
||||
checked={enabled}
|
||||
onCheckedChange={(checked) => {
|
||||
setModelParamEnabled("providerOptions", checked);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{enabled && (
|
||||
<div>
|
||||
<CodeMirrorEditor
|
||||
value={inputValue}
|
||||
onChange={(value) => {
|
||||
setInputValue(value);
|
||||
|
||||
try {
|
||||
const parsed = JSONObjectSchema.parse(JSON.parse(value));
|
||||
updateModelParam("providerOptions", parsed);
|
||||
setError(null);
|
||||
} catch {
|
||||
setError("Invalid JSON Object");
|
||||
}
|
||||
}}
|
||||
editable={enabled && !formDisabled}
|
||||
mode="json"
|
||||
minHeight="none"
|
||||
lineNumbers={false}
|
||||
/>
|
||||
{error && (
|
||||
<span className="pt-6">
|
||||
<p className="text-[12px] text-red-500">{error}</p>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ export const PeekViewEvaluatorConfigDetail = ({
|
||||
<span className="max-h-fit text-lg font-medium">Configuration</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusBadge
|
||||
type={evalConfig.status.toLowerCase()}
|
||||
type={evalConfig.finalStatus.toLowerCase()}
|
||||
isLive
|
||||
className="max-h-8"
|
||||
/>
|
||||
|
||||
@@ -148,7 +148,16 @@ export default function ObservationsTable({
|
||||
column: "type",
|
||||
type: "stringOptions",
|
||||
operator: "any of",
|
||||
value: ["GENERATION"],
|
||||
value: [
|
||||
"GENERATION",
|
||||
"AGENT",
|
||||
"TOOL",
|
||||
"CHAIN",
|
||||
"RETRIEVER",
|
||||
"EVALUATOR",
|
||||
"EMBEDDING",
|
||||
"GUARDRAIL",
|
||||
],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { PrettyJsonView } from "@/src/components/ui/PrettyJsonView";
|
||||
import { AnnotationQueueObjectType, type APIScoreV2 } from "@langfuse/shared";
|
||||
import {
|
||||
AnnotationQueueObjectType,
|
||||
type APIScoreV2,
|
||||
isGenerationLike,
|
||||
} from "@langfuse/shared";
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import { api } from "@/src/utils/api";
|
||||
@@ -177,14 +181,15 @@ export const ObservationPreview = ({
|
||||
objectType={AnnotationQueueObjectType.OBSERVATION}
|
||||
/>
|
||||
</div>
|
||||
{observationWithInputAndOutput.data?.type === "GENERATION" && (
|
||||
<JumpToPlaygroundButton
|
||||
source="generation"
|
||||
generation={observationWithInputAndOutput.data}
|
||||
analyticsEventName="trace_detail:test_in_playground_button_click"
|
||||
className={cn(isTimeline ? "!hidden" : "")}
|
||||
/>
|
||||
)}
|
||||
{observationWithInputAndOutput.data &&
|
||||
isGenerationLike(observationWithInputAndOutput.data.type) && (
|
||||
<JumpToPlaygroundButton
|
||||
source="generation"
|
||||
generation={observationWithInputAndOutput.data}
|
||||
analyticsEventName="trace_detail:test_in_playground_button_click"
|
||||
className={cn(isTimeline ? "!hidden" : "")}
|
||||
/>
|
||||
)}
|
||||
<CommentDrawerButton
|
||||
projectId={preloadedObservation.projectId}
|
||||
objectId={preloadedObservation.id}
|
||||
@@ -259,7 +264,7 @@ export const ObservationPreview = ({
|
||||
projectId={preloadedObservation.projectId}
|
||||
/>
|
||||
) : undefined}
|
||||
{preloadedObservation.type === "GENERATION" && (
|
||||
{isGenerationLike(preloadedObservation.type) && (
|
||||
<BreakdownTooltip
|
||||
details={preloadedObservation.usageDetails}
|
||||
isCost={false}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type APIScoreV2,
|
||||
type TraceDomain,
|
||||
AnnotationQueueObjectType,
|
||||
isGenerationLike,
|
||||
} from "@langfuse/shared";
|
||||
import { AggUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
@@ -102,7 +103,7 @@ export const TracePreview = ({
|
||||
const usageDetails = useMemo(
|
||||
() =>
|
||||
observations
|
||||
.filter((o) => o.type === "GENERATION")
|
||||
.filter((o) => isGenerationLike(o.type))
|
||||
.map((o) => o.usageDetails),
|
||||
[observations],
|
||||
);
|
||||
@@ -201,7 +202,7 @@ export const TracePreview = ({
|
||||
{totalCost && (
|
||||
<BreakdownTooltip
|
||||
details={observations
|
||||
.filter((o) => o.type === "GENERATION")
|
||||
.filter((o) => isGenerationLike(o.type))
|
||||
.map((o) => o.costDetails)}
|
||||
isCost
|
||||
>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
@@ -33,7 +32,7 @@ const RadioGroupItem = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Check className="h-3.5 w-3.5 fill-primary" />
|
||||
<div className="h-2 w-2 rounded-full bg-primary" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v3.95.2";
|
||||
export const VERSION = "v3.98.2";
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@/src/components/ui/button";
|
||||
import { MultiSelectCombobox } from "@/src/components/ui/multi-select-combobox";
|
||||
import { useUserSearch } from "@/src/features/annotation-queues/hooks/useUserSearch";
|
||||
import { useSelectedUsers } from "@/src/features/annotation-queues/hooks/useSelectedUsers";
|
||||
import { showSuccessToast } from "@/src/features/notifications/showSuccessToast";
|
||||
|
||||
interface UserAssignmentSectionProps {
|
||||
projectId: string;
|
||||
@@ -27,6 +28,7 @@ export const UserAssignmentSection = ({
|
||||
projectId: projectId,
|
||||
scope: "annotationQueueAssignments:CUD",
|
||||
});
|
||||
const utils = api.useUtils();
|
||||
|
||||
// Get current assigned users
|
||||
const queueAssignmentsQuery =
|
||||
@@ -35,6 +37,18 @@ export const UserAssignmentSection = ({
|
||||
{ enabled: !!queueId && hasQueueAssignmentsReadAccess },
|
||||
);
|
||||
|
||||
const deleteQueueAssignmentMutation =
|
||||
api.annotationQueueAssignments.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.annotationQueueAssignments.invalidate();
|
||||
utils.annotationQueues.invalidate();
|
||||
showSuccessToast({
|
||||
title: "Removed assignment",
|
||||
description: "User removed from queue successfully",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Combine selected users and assigned users for exclusion
|
||||
const assignedUserIds =
|
||||
queueAssignmentsQuery.data?.assignments.map((user: any) => user.id) || [];
|
||||
@@ -56,6 +70,16 @@ export const UserAssignmentSection = ({
|
||||
onChange(userIds);
|
||||
};
|
||||
|
||||
// Handle user removal
|
||||
const handleUserRemove = (userId: string) => {
|
||||
if (!!queueId)
|
||||
deleteQueueAssignmentMutation.mutate({
|
||||
projectId,
|
||||
queueId,
|
||||
userId,
|
||||
});
|
||||
};
|
||||
|
||||
// Check if there are more assigned users than shown
|
||||
const hasMoreAssignedUsers =
|
||||
queueAssignmentsQuery.data &&
|
||||
@@ -133,9 +157,17 @@ export const UserAssignmentSection = ({
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* <Button variant="ghost" size="icon-sm">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
disabled={
|
||||
!hasQueueAssignmentWriteAccess ||
|
||||
deleteQueueAssignmentMutation.isLoading
|
||||
}
|
||||
onClick={() => handleUserRemove(user.id)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button> */}
|
||||
</Button>
|
||||
</div>
|
||||
{(index <
|
||||
queueAssignmentsQuery.data?.assignments.length - 1 ||
|
||||
|
||||
@@ -66,7 +66,7 @@ export function RequestResetPasswordEmailButton({
|
||||
const callback = encodeURIComponent(
|
||||
`${env.NEXT_PUBLIC_BASE_PATH ?? ""}/auth/reset-password`,
|
||||
);
|
||||
const url = `/api/auth/callback/email?email=${formattedEmail}&token=${formattedCode}&callbackUrl=${callback}`;
|
||||
const url = `${env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/auth/callback/email?email=${formattedEmail}&token=${formattedCode}&callbackUrl=${callback}`;
|
||||
window.location.href = url;
|
||||
} catch (error) {
|
||||
console.error("Error verifying code:", error);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { api } from "@/src/utils/api";
|
||||
import { type FilterState } from "@langfuse/shared";
|
||||
import { type FilterState, getGenerationLikeTypes } from "@langfuse/shared";
|
||||
import {
|
||||
extractTimeSeriesData,
|
||||
fillMissingValuesAndTransform,
|
||||
@@ -69,9 +69,9 @@ export const GenerationLatencyChart = ({
|
||||
...mapLegacyUiTableFilterToView("observations", globalFilterState),
|
||||
{
|
||||
column: "type",
|
||||
operator: "=",
|
||||
value: "GENERATION",
|
||||
type: "string",
|
||||
operator: "any of",
|
||||
value: getGenerationLikeTypes(),
|
||||
type: "stringOptions",
|
||||
},
|
||||
{
|
||||
column: "providedModelName",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RightAlignedCell } from "@/src/features/dashboard/components/RightAlignedCell";
|
||||
import { DashboardCard } from "@/src/features/dashboard/components/cards/DashboardCard";
|
||||
import { DashboardTable } from "@/src/features/dashboard/components/cards/DashboardTable";
|
||||
import { type FilterState } from "@langfuse/shared";
|
||||
import { type FilterState, getGenerationLikeTypes } from "@langfuse/shared";
|
||||
import { api } from "@/src/utils/api";
|
||||
|
||||
import { formatIntervalSeconds } from "@/src/utils/dates";
|
||||
@@ -38,9 +38,9 @@ export const LatencyTables = ({
|
||||
...mapLegacyUiTableFilterToView("observations", globalFilterState),
|
||||
{
|
||||
column: "type",
|
||||
operator: "=",
|
||||
value: "GENERATION",
|
||||
type: "string",
|
||||
operator: "any of",
|
||||
value: getGenerationLikeTypes(),
|
||||
type: "stringOptions",
|
||||
},
|
||||
],
|
||||
timeDimension: null,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { RightAlignedCell } from "@/src/features/dashboard/components/RightAlign
|
||||
import { LeftAlignedCell } from "@/src/features/dashboard/components/LeftAlignedCell";
|
||||
import { DashboardCard } from "@/src/features/dashboard/components/cards/DashboardCard";
|
||||
import { DashboardTable } from "@/src/features/dashboard/components/cards/DashboardTable";
|
||||
import { type FilterState } from "@langfuse/shared";
|
||||
import { type FilterState, getGenerationLikeTypes } from "@langfuse/shared";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { compactNumberFormatter } from "@/src/utils/numbers";
|
||||
import { TotalMetric } from "./TotalMetric";
|
||||
@@ -40,9 +40,9 @@ export const ModelCostTable = ({
|
||||
...mapLegacyUiTableFilterToView("observations", globalFilterState),
|
||||
{
|
||||
column: "type",
|
||||
operator: "=",
|
||||
value: "GENERATION",
|
||||
type: "string",
|
||||
operator: "any of",
|
||||
value: getGenerationLikeTypes(),
|
||||
type: "stringOptions",
|
||||
},
|
||||
],
|
||||
timeDimension: null,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
dashboardDateRangeAggregationSettings,
|
||||
} from "@/src/utils/date-range-utils";
|
||||
import { compactNumberFormatter } from "@/src/utils/numbers";
|
||||
import { type FilterState } from "@langfuse/shared";
|
||||
import { type FilterState, getGenerationLikeTypes } from "@langfuse/shared";
|
||||
import {
|
||||
ModelSelectorPopover,
|
||||
useModelSelection,
|
||||
@@ -70,9 +70,9 @@ export const ModelUsageChart = ({
|
||||
...mapLegacyUiTableFilterToView("observations", userAndEnvFilterState),
|
||||
{
|
||||
column: "type",
|
||||
operator: "=",
|
||||
value: "GENERATION",
|
||||
type: "string",
|
||||
operator: "any of",
|
||||
value: getGenerationLikeTypes(),
|
||||
type: "stringOptions",
|
||||
},
|
||||
{
|
||||
column: "providedModelName",
|
||||
@@ -115,7 +115,12 @@ export const ModelUsageChart = ({
|
||||
],
|
||||
filter: [
|
||||
...globalFilterState,
|
||||
{ type: "string", column: "type", operator: "=", value: "GENERATION" },
|
||||
{
|
||||
type: "stringOptions",
|
||||
column: "type",
|
||||
operator: "any of",
|
||||
value: getGenerationLikeTypes(),
|
||||
},
|
||||
{
|
||||
type: "stringOptions",
|
||||
column: "model",
|
||||
@@ -160,7 +165,12 @@ export const ModelUsageChart = ({
|
||||
],
|
||||
filter: [
|
||||
...globalFilterState,
|
||||
{ type: "string", column: "type", operator: "=", value: "GENERATION" },
|
||||
{
|
||||
type: "stringOptions",
|
||||
column: "type",
|
||||
operator: "any of",
|
||||
value: getGenerationLikeTypes(),
|
||||
},
|
||||
{
|
||||
type: "stringOptions",
|
||||
column: "model",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { api } from "@/src/utils/api";
|
||||
import { type FilterState } from "@langfuse/shared";
|
||||
import { type FilterState, getGenerationLikeTypes } from "@langfuse/shared";
|
||||
import { DashboardCard } from "@/src/features/dashboard/components/cards/DashboardCard";
|
||||
import { compactNumberFormatter } from "@/src/utils/numbers";
|
||||
import { TabComponent } from "@/src/features/dashboard/components/TabsComponent";
|
||||
@@ -46,9 +46,9 @@ export const UserChart = ({
|
||||
...mapLegacyUiTableFilterToView("observations", globalFilterState),
|
||||
{
|
||||
column: "type",
|
||||
operator: "=",
|
||||
value: "GENERATION",
|
||||
type: "string",
|
||||
operator: "any of",
|
||||
value: getGenerationLikeTypes(),
|
||||
type: "stringOptions",
|
||||
},
|
||||
],
|
||||
timeDimension: null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type TimeSeriesChartDataPoint } from "@/src/features/dashboard/components/BaseTimeSeriesChart";
|
||||
import { type FilterState } from "@langfuse/shared";
|
||||
import { type FilterState, getGenerationLikeTypes } from "@langfuse/shared";
|
||||
import { type DatabaseRow } from "@/src/server/api/services/sqlInterface";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { mapLegacyUiTableFilterToView } from "@/src/features/query";
|
||||
@@ -21,9 +21,9 @@ export const getAllModels = (
|
||||
...mapLegacyUiTableFilterToView("observations", globalFilterState),
|
||||
{
|
||||
column: "type",
|
||||
operator: "=",
|
||||
value: "GENERATION",
|
||||
type: "string",
|
||||
operator: "any of",
|
||||
value: getGenerationLikeTypes(),
|
||||
type: "stringOptions",
|
||||
},
|
||||
],
|
||||
timeDimension: null,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/src/components/ui/dialog";
|
||||
import { useState } from "react";
|
||||
import { useState, forwardRef } from "react";
|
||||
import { DialogTrigger } from "@radix-ui/react-dialog";
|
||||
import { DatasetForm } from "@/src/features/datasets/components/DatasetForm";
|
||||
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
@@ -46,7 +46,10 @@ type DatasetActionButtonProps =
|
||||
| UpdateDatasetButtonProps
|
||||
| DeleteDatasetButtonProps;
|
||||
|
||||
export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
export const DatasetActionButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
DatasetActionButtonProps
|
||||
>((props, ref) => {
|
||||
const capture = usePostHogClientCapture();
|
||||
const [open, setOpen] = useState(false);
|
||||
const hasAccess = useHasProjectAccess({
|
||||
@@ -60,6 +63,7 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
{props.mode === "update" ? (
|
||||
props.icon ? (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={props.variant || "outline"}
|
||||
size={props.size || "icon"}
|
||||
className={props.className}
|
||||
@@ -74,6 +78,7 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={props.variant || "ghost"}
|
||||
size={props.size || "icon"}
|
||||
className={props.className}
|
||||
@@ -95,6 +100,7 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
)
|
||||
) : props.mode === "delete" ? (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={props.variant || "ghost"}
|
||||
size={props.size}
|
||||
className={props.className}
|
||||
@@ -115,6 +121,7 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
ref={ref}
|
||||
size={props.size}
|
||||
className={props.className}
|
||||
disabled={!hasAccess}
|
||||
@@ -175,4 +182,6 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
DatasetActionButton.displayName = "DatasetActionButton";
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DataTable } from "@/src/components/table/data-table";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
|
||||
import { Archive, ListTree, MoreVertical, Trash2 } from "lucide-react";
|
||||
import { Archive, Edit, ListTree, MoreVertical, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { type DatasetItem, DatasetStatus, type Prisma } from "@langfuse/shared";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
@@ -53,6 +54,7 @@ export function DatasetItemsTable({
|
||||
datasetId: string;
|
||||
menuItems?: React.ReactNode;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
const utils = api.useUtils();
|
||||
const capture = usePostHogClientCapture();
|
||||
@@ -229,6 +231,17 @@ export function DatasetItemsTable({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
disabled={!hasAccess}
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/project/${projectId}/datasets/${datasetId}/items/${id}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
disabled={!hasAccess}
|
||||
onClick={() => {
|
||||
|
||||
@@ -214,6 +214,15 @@ export function parseColumns(
|
||||
headerMap: Map<string, number>,
|
||||
): Prisma.JsonValue {
|
||||
if (columnNames.length === 0) return null;
|
||||
|
||||
// Single column: do not nest columns into json objects
|
||||
if (columnNames.length === 1) {
|
||||
const col = columnNames[0];
|
||||
const rawValue = row[headerMap.get(col)!];
|
||||
return parseValue(rawValue);
|
||||
}
|
||||
|
||||
// Multiple columns: nest columns into json objects
|
||||
return Object.fromEntries(
|
||||
columnNames.map((col) => [col, parseValue(row[headerMap.get(col)!])]),
|
||||
);
|
||||
|
||||
@@ -410,7 +410,7 @@ export const datasetRouter = createTRPCRouter({
|
||||
// Get all runs from PostgreSQL and merge with ClickHouse metrics to maintain consistent count
|
||||
const [runsWithMetrics, totalRuns, allRunsBasicInfo] =
|
||||
await Promise.all([
|
||||
// Get runs that have metrics (only runs with dataset_run_items)
|
||||
// Get runs that have metrics (only runs with dataset_run_items_rmt)
|
||||
getDatasetRunsTableMetricsCh({
|
||||
projectId: queryInput.projectId,
|
||||
datasetId: queryInput.datasetId,
|
||||
@@ -420,14 +420,14 @@ export const datasetRouter = createTRPCRouter({
|
||||
? queryInput.page * queryInput.limit
|
||||
: undefined,
|
||||
}),
|
||||
// Count all runs (including those without dataset_run_items)
|
||||
// Count all runs (including those without dataset_run_items_rmt)
|
||||
ctx.prisma.datasetRuns.count({
|
||||
where: {
|
||||
datasetId: queryInput.datasetId,
|
||||
projectId: queryInput.projectId,
|
||||
},
|
||||
}),
|
||||
// Get basic info for all runs to ensure we return all runs, even those without dataset_run_items
|
||||
// Get basic info for all runs to ensure we return all runs, even those without dataset_run_items_rmt
|
||||
ctx.prisma.datasetRuns.findMany({
|
||||
where: {
|
||||
datasetId: queryInput.datasetId,
|
||||
@@ -460,7 +460,7 @@ export const datasetRouter = createTRPCRouter({
|
||||
runsWithMetrics.map((run) => [run.id, run]),
|
||||
);
|
||||
|
||||
// Only fetch scores for runs that have metrics (runs without dataset_run_items won't have trace scores)
|
||||
// Only fetch scores for runs that have metrics (runs without dataset_run_items_rmt won't have trace scores)
|
||||
const runsWithMetricsIds = runsWithMetrics.map((run) => run.id);
|
||||
const [traceScores, runScores] = await Promise.all([
|
||||
runsWithMetricsIds.length > 0
|
||||
@@ -483,7 +483,7 @@ export const datasetRouter = createTRPCRouter({
|
||||
|
||||
return {
|
||||
...run,
|
||||
// Use ClickHouse metrics if available, otherwise use defaults for runs without dataset_run_items
|
||||
// Use ClickHouse metrics if available, otherwise use defaults for runs without dataset_run_items_rmt
|
||||
countRunItems: metrics?.countRunItems ?? 0,
|
||||
avgTotalCost: metrics?.avgTotalCost ?? null,
|
||||
avgLatency: metrics?.avgLatency ?? null,
|
||||
|
||||
@@ -33,9 +33,6 @@ export const EvalTemplateDetail = () => {
|
||||
const templateId = router.query.id as string;
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<EvalTemplate | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
// get the current template by id
|
||||
const template = api.evals.templateById.useQuery({
|
||||
@@ -58,15 +55,7 @@ export const EvalTemplateDetail = () => {
|
||||
},
|
||||
);
|
||||
|
||||
// Set the selected template when data is loaded
|
||||
React.useEffect(() => {
|
||||
if (template.data && !selectedTemplate) {
|
||||
setSelectedTemplate(template.data);
|
||||
}
|
||||
}, [template.data, selectedTemplate]);
|
||||
|
||||
const handleTemplateSelect = (newTemplate: EvalTemplate) => {
|
||||
setSelectedTemplate(newTemplate);
|
||||
// Update URL without full page reload
|
||||
router.push(
|
||||
`/project/${projectId}/evals/templates/${newTemplate.id}`,
|
||||
@@ -75,13 +64,10 @@ export const EvalTemplateDetail = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Get the appropriate template to display
|
||||
const displayTemplate = selectedTemplate || template.data;
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerProps={{
|
||||
title: `${displayTemplate?.name || ""}`,
|
||||
title: `${template.data?.name ?? ""}`,
|
||||
itemType: "EVALUATOR",
|
||||
breadcrumb: [
|
||||
{
|
||||
@@ -95,7 +81,7 @@ export const EvalTemplateDetail = () => {
|
||||
projectId={projectId}
|
||||
isEditing={isEditing}
|
||||
setIsEditing={setIsEditing}
|
||||
isCustom={!!displayTemplate?.projectId}
|
||||
isCustom={!!template.data?.projectId}
|
||||
/>
|
||||
|
||||
{/* TODO: moved to LFE-4573 */}
|
||||
@@ -114,14 +100,14 @@ export const EvalTemplateDetail = () => {
|
||||
),
|
||||
}}
|
||||
>
|
||||
{allTemplates.isLoading || !allTemplates.data || !displayTemplate ? (
|
||||
{allTemplates.isLoading || !allTemplates.data || !template.data ? (
|
||||
<div className="p-3">Loading...</div>
|
||||
) : isEditing ? (
|
||||
<div className="overflow-y-auto p-3 pt-1">
|
||||
<EvalTemplateForm
|
||||
useDialog={false}
|
||||
projectId={projectId}
|
||||
existingEvalTemplate={displayTemplate}
|
||||
existingEvalTemplate={template.data}
|
||||
isEditing={isEditing}
|
||||
setIsEditing={setIsEditing}
|
||||
/>
|
||||
@@ -132,7 +118,7 @@ export const EvalTemplateDetail = () => {
|
||||
<EvalTemplateForm
|
||||
useDialog={false}
|
||||
projectId={projectId}
|
||||
existingEvalTemplate={displayTemplate}
|
||||
existingEvalTemplate={template.data}
|
||||
isEditing={isEditing}
|
||||
setIsEditing={setIsEditing}
|
||||
/>
|
||||
@@ -150,7 +136,7 @@ export const EvalTemplateDetail = () => {
|
||||
<div
|
||||
key={template.id}
|
||||
className={`flex cursor-pointer flex-col rounded-md px-2 py-1.5 hover:bg-accent ${
|
||||
template.id === displayTemplate.id ? "bg-accent" : ""
|
||||
template.id === templateId ? "bg-accent" : ""
|
||||
}`}
|
||||
onClick={() => handleTemplateSelect(template)}
|
||||
>
|
||||
|
||||
@@ -58,6 +58,12 @@ export function EvaluatorSelector({
|
||||
},
|
||||
);
|
||||
|
||||
// Ensure per-name arrays are sorted by createdAt ascending so last is latest
|
||||
const sortByCreatedAt = (arr: EvalTemplate[]) =>
|
||||
arr.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
||||
Object.values(groupedTemplates.custom).forEach(sortByCreatedAt);
|
||||
Object.values(groupedTemplates.langfuse).forEach(sortByCreatedAt);
|
||||
|
||||
// Filter templates based on search
|
||||
const filteredTemplates = {
|
||||
langfuse: Object.entries(groupedTemplates.langfuse)
|
||||
|
||||
@@ -15,6 +15,7 @@ import { DeleteEvaluationModelButton } from "@/src/components/deleteButton";
|
||||
import { ManageDefaultEvalModel } from "@/src/features/evals/components/manage-default-eval-model";
|
||||
import { useState } from "react";
|
||||
import { DialogContent, DialogTrigger } from "@/src/components/ui/dialog";
|
||||
import { getFinalModelParams } from "@/src/utils/getFinalModelParams";
|
||||
import { Dialog } from "@/src/components/ui/dialog";
|
||||
import { Pencil } from "lucide-react";
|
||||
import {
|
||||
@@ -30,6 +31,7 @@ export default function DefaultEvaluationModelPage() {
|
||||
const projectId = router.query.projectId as string;
|
||||
const utils = api.useUtils();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
const hasWriteAccess = useHasProjectAccess({
|
||||
projectId,
|
||||
@@ -56,7 +58,7 @@ export default function DefaultEvaluationModelPage() {
|
||||
setModelParams,
|
||||
);
|
||||
|
||||
const { mutate: upsertDefaultModel, isLoading } =
|
||||
const { mutateAsync: upsertDefaultModel, isLoading } =
|
||||
api.defaultLlmModel.upsertDefaultModel.useMutation({
|
||||
onSuccess: () => {
|
||||
showSuccessToast({
|
||||
@@ -65,26 +67,22 @@ export default function DefaultEvaluationModelPage() {
|
||||
});
|
||||
|
||||
utils.defaultLlmModel.fetchDefaultModel.invalidate({ projectId });
|
||||
setFormError(null);
|
||||
setIsEditing(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
setFormError(error.message as string);
|
||||
},
|
||||
});
|
||||
|
||||
const executeUpsertMutation = () => {
|
||||
try {
|
||||
upsertDefaultModel({
|
||||
projectId,
|
||||
provider: modelParams.provider.value,
|
||||
adapter: modelParams.adapter.value,
|
||||
model: modelParams.model.value,
|
||||
modelParams: {
|
||||
max_tokens: modelParams.max_tokens.value,
|
||||
temperature: modelParams.temperature.value,
|
||||
top_p: modelParams.top_p.value,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
setIsEditing(false);
|
||||
const executeUpsertMutation = async () => {
|
||||
await upsertDefaultModel({
|
||||
projectId,
|
||||
provider: modelParams.provider.value,
|
||||
adapter: modelParams.adapter.value,
|
||||
model: modelParams.model.value,
|
||||
modelParams: getFinalModelParams(modelParams),
|
||||
});
|
||||
};
|
||||
|
||||
if (isDefaultModelLoading) {
|
||||
@@ -135,7 +133,15 @@ export default function DefaultEvaluationModelPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dialog open={isEditing} onOpenChange={setIsEditing}>
|
||||
<Dialog
|
||||
open={isEditing}
|
||||
onOpenChange={(open) => {
|
||||
setIsEditing(open);
|
||||
if (!open) {
|
||||
setFormError(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
disabled={!hasWriteAccess}
|
||||
@@ -167,24 +173,31 @@ export default function DefaultEvaluationModelPage() {
|
||||
<div className="my-2 text-xs text-muted-foreground">
|
||||
Select a model which supports function calling.
|
||||
</div>
|
||||
<div className="mt-2 flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setIsEditing(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
{selectedModel ? (
|
||||
<UpdateButton
|
||||
projectId={projectId}
|
||||
isLoading={isLoading}
|
||||
executeUpsertMutation={executeUpsertMutation}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
disabled={!hasWriteAccess || !modelParams.provider.value}
|
||||
onClick={executeUpsertMutation}
|
||||
>
|
||||
Save
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="mt-2 flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setIsEditing(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
{selectedModel ? (
|
||||
<UpdateButton
|
||||
projectId={projectId}
|
||||
isLoading={isLoading}
|
||||
executeUpsertMutation={executeUpsertMutation}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
disabled={!hasWriteAccess || !modelParams.provider.value}
|
||||
onClick={executeUpsertMutation}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{formError ? (
|
||||
<p className="text-red w-full text-center">
|
||||
<span className="font-bold">Error:</span> {formError}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -223,7 +236,10 @@ function UpdateButton({
|
||||
Update
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent onClick={(e) => e.stopPropagation()}>
|
||||
<PopoverContent
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-fit max-w-[500px]"
|
||||
>
|
||||
<h2 className="text-md mb-3 font-semibold">Please confirm</h2>
|
||||
<p className="mb-3 text-sm">
|
||||
Updating the default model will impact any currently running
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from "@/src/server/api/trpc";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
ForbiddenError,
|
||||
InvalidRequestError,
|
||||
LangfuseNotFoundError,
|
||||
ZodModelConfig,
|
||||
@@ -42,12 +43,14 @@ export const defaultEvalModelRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
try {
|
||||
return DefaultEvalModelService.upsertDefaultModel(input);
|
||||
return await DefaultEvalModelService.upsertDefaultModel(input);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidRequestError) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: error.message });
|
||||
} else if (error instanceof LangfuseNotFoundError) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: error.message });
|
||||
} else if (error instanceof ForbiddenError) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: error.message });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { z } from "zod/v4";
|
||||
import { z as zodV3 } from "zod/v3";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
ZodModelConfig,
|
||||
singleFilter,
|
||||
variableMapping,
|
||||
ChatMessageRole,
|
||||
paginationZod,
|
||||
type JobConfiguration,
|
||||
JobType,
|
||||
@@ -21,19 +19,16 @@ import {
|
||||
orderBy,
|
||||
jsonSchema,
|
||||
} from "@langfuse/shared";
|
||||
import { decrypt } from "@langfuse/shared/encryption";
|
||||
import {
|
||||
decryptAndParseExtraHeaders,
|
||||
fetchLLMCompletion,
|
||||
getQueue,
|
||||
getScoresByIds,
|
||||
logger,
|
||||
QueueName,
|
||||
QueueJobs,
|
||||
ChatMessageType,
|
||||
tableColumnsToSqlFilterAndPrefix,
|
||||
orderByToPrismaSql,
|
||||
DefaultEvalModelService,
|
||||
testModelCall,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { EvalReferencedEvaluators } from "@/src/features/evals/types";
|
||||
@@ -808,45 +803,20 @@ export const evalRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const matchingLLMKey = modelConfig.config.apiKey;
|
||||
|
||||
// Make a test structured output call to validate the LLM key
|
||||
try {
|
||||
(
|
||||
await fetchLLMCompletion({
|
||||
streaming: false,
|
||||
apiKey: decrypt(matchingLLMKey.secretKey), // decrypt the secret key
|
||||
extraHeaders: decryptAndParseExtraHeaders(
|
||||
matchingLLMKey.extraHeaders,
|
||||
),
|
||||
baseURL: matchingLLMKey.baseURL ?? undefined,
|
||||
messages: [
|
||||
{
|
||||
role: ChatMessageRole.User,
|
||||
content: input.prompt,
|
||||
type: ChatMessageType.User,
|
||||
},
|
||||
],
|
||||
modelParams: {
|
||||
provider: modelConfig.config.provider,
|
||||
model: modelConfig.config.model,
|
||||
adapter: matchingLLMKey.adapter,
|
||||
...input.modelParams,
|
||||
},
|
||||
structuredOutputSchema: zodV3.object({
|
||||
score: zodV3.string(),
|
||||
reasoning: zodV3.string(),
|
||||
}),
|
||||
config: matchingLLMKey.config,
|
||||
})
|
||||
).completion;
|
||||
// Make a test structured output call to validate the LLM key
|
||||
await testModelCall({
|
||||
provider: modelConfig.config.provider,
|
||||
model: modelConfig.config.model,
|
||||
apiKey: modelConfig.config.apiKey,
|
||||
modelConfig: input.modelParams,
|
||||
prompt: input.prompt,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
throw new TRPCError({
|
||||
code: "PRECONDITION_FAILED",
|
||||
message:
|
||||
"Selected model is not supported for evaluations. Test tool call failed.",
|
||||
message: `Model configuration not valid for evaluation. ${message}`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -289,6 +289,7 @@ export const llmApiKeyRouter = createTRPCRouter({
|
||||
customModels: true,
|
||||
withDefaultModels: true,
|
||||
extraHeaderKeys: true,
|
||||
config: true,
|
||||
},
|
||||
where: {
|
||||
projectId: input.projectId,
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import type { Path } from "react-hook-form";
|
||||
import { useRouter } from "next/router";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Form } from "@/src/components/ui/form";
|
||||
import { LangfuseIcon } from "@/src/components/LangfuseLogo";
|
||||
import { useSurveyForm } from "../hooks/useSurveyForm";
|
||||
import { SurveyProgress } from "./SurveyProgress";
|
||||
import { SurveyStep } from "./SurveyStep";
|
||||
import type { SurveyFormData } from "../lib/surveyTypes";
|
||||
|
||||
export function OnboardingSurvey() {
|
||||
const router = useRouter();
|
||||
const {
|
||||
form,
|
||||
state,
|
||||
currentQuestion,
|
||||
isLastStep,
|
||||
isFirstStep,
|
||||
goNext,
|
||||
goBack,
|
||||
handleAutoAdvance,
|
||||
handleSubmit,
|
||||
totalSteps,
|
||||
} = useSurveyForm();
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: SurveyFormData) => {
|
||||
await handleSubmit(data);
|
||||
void router.push("/");
|
||||
},
|
||||
[handleSubmit, router],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Cmd+Enter (Mac) or Ctrl+Enter (Windows/Linux) on referralSource step submits the form
|
||||
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
||||
if (currentQuestion?.id === "referralSource") {
|
||||
event.preventDefault();
|
||||
form.handleSubmit(onSubmit)();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Regular Enter advances to next step (existing behavior)
|
||||
if (event.key === "Enter" && !isLastStep) {
|
||||
if (currentQuestion?.type !== "text") {
|
||||
goNext();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [
|
||||
isLastStep,
|
||||
currentQuestion?.type,
|
||||
currentQuestion?.id,
|
||||
goNext,
|
||||
form,
|
||||
onSubmit,
|
||||
]);
|
||||
|
||||
// Auto-focus the first form control of the current step
|
||||
useEffect(() => {
|
||||
if (!currentQuestion?.id) return;
|
||||
const field = currentQuestion.id as Path<SurveyFormData>;
|
||||
const raf = requestAnimationFrame(() => {
|
||||
try {
|
||||
form.setFocus(field, { shouldSelect: true });
|
||||
} catch {
|
||||
// ignore if the control cannot be focused
|
||||
}
|
||||
});
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, [currentQuestion?.id, form]);
|
||||
|
||||
// Determine labeling/behavior of the primary (right) button
|
||||
const roleValue = form.watch("role");
|
||||
const signupReasonValue = form.watch("signupReason");
|
||||
const referralSourceValue = form.watch("referralSource");
|
||||
|
||||
const currentFieldId = currentQuestion?.id as
|
||||
| keyof SurveyFormData
|
||||
| undefined;
|
||||
const currentValue = currentFieldId
|
||||
? form.watch(currentFieldId as Path<SurveyFormData>)
|
||||
: undefined;
|
||||
|
||||
const isEmpty = (v: unknown) =>
|
||||
v == null || (typeof v === "string" && v.trim() === "");
|
||||
const allFields = {
|
||||
role: roleValue,
|
||||
signupReason: signupReasonValue,
|
||||
referralSource: referralSourceValue,
|
||||
} as const;
|
||||
|
||||
const currentEmpty = isEmpty(currentValue);
|
||||
const otherTwoEmpty = Object.entries(allFields)
|
||||
.filter(([key]) => key !== currentFieldId)
|
||||
.every(([, v]) => isEmpty(v));
|
||||
// showSkip: ghost button labeled "Skip" when skipping is the intended action
|
||||
const showSkip = isLastStep ? currentEmpty && otherTwoEmpty : currentEmpty;
|
||||
|
||||
const handleSkipButton = () => {
|
||||
if (isLastStep) {
|
||||
void router.push("/");
|
||||
} else {
|
||||
goNext();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitButton = () => {
|
||||
if (isLastStep) {
|
||||
form.handleSubmit(onSubmit)();
|
||||
} else {
|
||||
goNext();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col py-6 sm:min-h-full sm:justify-start sm:px-6 sm:py-12 lg:px-8">
|
||||
<div className="flex items-center justify-center gap-2 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<LangfuseIcon className="h-8 w-8" />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-lg bg-background px-6 py-6 shadow sm:mx-auto sm:mt-16 sm:w-full sm:max-w-[480px] sm:px-12 sm:py-10">
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex h-full flex-col"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
{currentQuestion && (
|
||||
<SurveyStep
|
||||
question={currentQuestion}
|
||||
control={form.control}
|
||||
onAutoAdvance={handleAutoAdvance}
|
||||
isLast={isLastStep}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row-reverse items-center justify-between pt-6">
|
||||
{showSkip ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSkipButton}
|
||||
variant="ghost"
|
||||
className="w-20"
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSubmitButton}
|
||||
variant="default"
|
||||
className="w-20"
|
||||
>
|
||||
{isLastStep ? "Finish" : "Next"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="basis-[10rem] px-4">
|
||||
<SurveyProgress
|
||||
currentStep={state.currentStep}
|
||||
totalSteps={totalSteps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isFirstStep ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={goBack}
|
||||
className="w-20"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
) : (
|
||||
<div className="w-20" />
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
interface SurveyProgressProps {
|
||||
currentStep: number;
|
||||
totalSteps: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SurveyProgress({
|
||||
currentStep,
|
||||
totalSteps,
|
||||
className,
|
||||
}: SurveyProgressProps) {
|
||||
return (
|
||||
<div className={cn("flex w-full gap-3", className)}>
|
||||
{Array.from({ length: totalSteps }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"h-1 flex-1 rounded-full transition-colors duration-300",
|
||||
index <= currentStep ? "bg-primary" : "bg-muted",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/src/components/ui/form";
|
||||
import { RadioGroup, RadioGroupItem } from "@/src/components/ui/radio-group";
|
||||
import { Textarea } from "@/src/components/ui/textarea";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import { Label } from "@/src/components/ui/label";
|
||||
import type { Control, Path } from "react-hook-form";
|
||||
import type { SurveyQuestion, SurveyFormData } from "../lib/surveyTypes";
|
||||
|
||||
const AUTO_ADVANCE_DELAY = 300;
|
||||
|
||||
interface SurveyStepProps {
|
||||
question: SurveyQuestion;
|
||||
control: Control<SurveyFormData>;
|
||||
onAutoAdvance?: (selectedValue?: string) => void;
|
||||
isLast?: boolean;
|
||||
}
|
||||
|
||||
export function SurveyStep({
|
||||
question,
|
||||
control,
|
||||
onAutoAdvance,
|
||||
isLast = false,
|
||||
}: SurveyStepProps) {
|
||||
const fieldName = question.id as keyof SurveyFormData;
|
||||
|
||||
const handleAutoAdvanceWithTimeout = (selectedValue?: string) => {
|
||||
if (onAutoAdvance) {
|
||||
// For signupReason question, ignore isLast and let the hook decide
|
||||
// For other questions, respect the isLast prop
|
||||
const shouldAutoAdvance = question.id === "signupReason" || !isLast;
|
||||
|
||||
if (shouldAutoAdvance) {
|
||||
setTimeout(() => {
|
||||
onAutoAdvance(selectedValue);
|
||||
}, AUTO_ADVANCE_DELAY);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (question.type === "radio") {
|
||||
return (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
control={control}
|
||||
name={fieldName as Path<SurveyFormData>}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col gap-2">
|
||||
<FormLabel className="text-xl font-semibold">
|
||||
{question.question}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
setTimeout(() => {
|
||||
handleAutoAdvanceWithTimeout(value);
|
||||
}, 0);
|
||||
}}
|
||||
value={field.value as string}
|
||||
className="grid gap-3"
|
||||
>
|
||||
{question.options.map((option) => (
|
||||
<Label
|
||||
key={option}
|
||||
htmlFor={option}
|
||||
className="flex flex-1 cursor-pointer items-center gap-3 rounded-lg border border-border p-3 text-sm font-medium leading-none transition-colors hover:bg-muted/50 peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
<RadioGroupItem value={option} id={option} />
|
||||
<span className="flex-1">{option}</span>
|
||||
</Label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (question.type === "text") {
|
||||
return (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
control={control}
|
||||
name={fieldName as Path<SurveyFormData>}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col gap-2">
|
||||
<FormLabel className="text-xl font-semibold">
|
||||
{question.question}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
{question.id === "referralSource" ? (
|
||||
<Input placeholder={question.placeholder} {...field} />
|
||||
) : (
|
||||
<Textarea
|
||||
placeholder={question.placeholder}
|
||||
className="min-h-[170px] resize-none"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useReducer, useCallback } from "react";
|
||||
import type { SurveyFormData } from "../lib/surveyTypes";
|
||||
import { surveyReducer, initialSurveyState } from "../lib/surveyReducer";
|
||||
import { SURVEY_QUESTIONS, TOTAL_STEPS } from "../lib/questions";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { SurveyName } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { showErrorToast } from "@/src/features/notifications/showErrorToast";
|
||||
import { showSuccessToast } from "@/src/features/notifications/showSuccessToast";
|
||||
|
||||
export function useSurveyForm() {
|
||||
const [state, dispatch] = useReducer(surveyReducer, initialSurveyState);
|
||||
const { data: session } = useSession();
|
||||
const createSurveyMutation = api.surveys.create.useMutation({
|
||||
onSuccess: () => {
|
||||
showSuccessToast({
|
||||
title: "Survey submitted",
|
||||
description: "Thank you for your feedback!",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorToast(
|
||||
"Failed to submit survey",
|
||||
error.message || "Please try again later.",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<SurveyFormData>({
|
||||
defaultValues: {
|
||||
role: undefined,
|
||||
signupReason: undefined,
|
||||
referralSource: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// Check if we should skip the referral source question
|
||||
const signupReason = form.watch("signupReason");
|
||||
const shouldSkipReferralQuestion = signupReason === "Invited by team";
|
||||
|
||||
// Get effective total steps (skip last question if invited by team)
|
||||
const effectiveTotalSteps = shouldSkipReferralQuestion
|
||||
? TOTAL_STEPS - 1
|
||||
: TOTAL_STEPS;
|
||||
|
||||
// Get current question, but skip the referral question if "Invited by team" is selected
|
||||
const currentQuestion = SURVEY_QUESTIONS[state.currentStep];
|
||||
|
||||
const isLastStep = state.currentStep === effectiveTotalSteps - 1;
|
||||
const isFirstStep = state.currentStep === 0;
|
||||
|
||||
const goNext = useCallback(() => {
|
||||
dispatch({ type: "next" });
|
||||
}, []);
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
dispatch({ type: "back" });
|
||||
}, []);
|
||||
|
||||
const goToStep = useCallback((step: number) => {
|
||||
dispatch({ type: "goToStep", step });
|
||||
}, []);
|
||||
|
||||
const handleAutoAdvance = useCallback(
|
||||
(selectedValue?: string) => {
|
||||
// Special case: if we're on step 1 (signup reason) and "Invited by team" was selected,
|
||||
// don't auto-advance, let the user click "Finish"
|
||||
if (state.currentStep === 1 && selectedValue === "Invited by team") {
|
||||
return;
|
||||
}
|
||||
|
||||
// For all other cases, advance to next step
|
||||
goNext();
|
||||
},
|
||||
[state.currentStep, goNext],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (data: SurveyFormData) => {
|
||||
const transformedResponse: Record<string, string> = {};
|
||||
if (data.role) transformedResponse["role"] = data.role;
|
||||
if (data.signupReason)
|
||||
transformedResponse["signupReason"] = data.signupReason;
|
||||
if (data.referralSource)
|
||||
transformedResponse["referralSource"] = data.referralSource.trim();
|
||||
|
||||
try {
|
||||
await createSurveyMutation.mutateAsync({
|
||||
surveyName: SurveyName.USER_ONBOARDING,
|
||||
response: transformedResponse,
|
||||
orgId: session?.user?.organizations?.[0]?.id,
|
||||
});
|
||||
} catch (error) {
|
||||
// Error handling is done in the mutation callbacks
|
||||
// This catch block is for any additional error handling if needed
|
||||
}
|
||||
},
|
||||
[createSurveyMutation, session],
|
||||
);
|
||||
|
||||
return {
|
||||
form,
|
||||
state,
|
||||
currentQuestion,
|
||||
isLastStep,
|
||||
isFirstStep,
|
||||
goNext,
|
||||
goBack,
|
||||
goToStep,
|
||||
handleAutoAdvance,
|
||||
handleSubmit,
|
||||
totalSteps: effectiveTotalSteps,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { SurveyQuestion } from "./surveyTypes";
|
||||
|
||||
export const SURVEY_QUESTIONS: SurveyQuestion[] = [
|
||||
{
|
||||
id: "role",
|
||||
type: "radio",
|
||||
question: "What describes you best?",
|
||||
options: [
|
||||
"Software Engineer",
|
||||
"ML Engineer / Data Scientist",
|
||||
"Product Manager",
|
||||
"Domain Expert",
|
||||
"Executive or Manager",
|
||||
"Other",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "signupReason",
|
||||
type: "radio",
|
||||
question: "Why are you signing up?",
|
||||
options: [
|
||||
"Invited by team",
|
||||
"Just looking around",
|
||||
"Evaluating / Testing Langfuse",
|
||||
"Start using Langfuse",
|
||||
"Migrating from other solution",
|
||||
"Migrating from self-hosted",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "referralSource",
|
||||
type: "text",
|
||||
question: "Where did you hear about us?",
|
||||
placeholder: "GitHub, X, Reddit, colleague etc.",
|
||||
},
|
||||
];
|
||||
|
||||
export const TOTAL_STEPS = SURVEY_QUESTIONS.length;
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { SurveyState, SurveyAction } from "./surveyTypes";
|
||||
import { TOTAL_STEPS } from "./questions";
|
||||
|
||||
export const initialSurveyState: SurveyState = {
|
||||
currentStep: 0,
|
||||
};
|
||||
|
||||
export function surveyReducer(
|
||||
state: SurveyState,
|
||||
action: SurveyAction,
|
||||
): SurveyState {
|
||||
switch (action.type) {
|
||||
case "next":
|
||||
return {
|
||||
...state,
|
||||
currentStep: Math.min(state.currentStep + 1, TOTAL_STEPS - 1),
|
||||
};
|
||||
case "back":
|
||||
return {
|
||||
...state,
|
||||
currentStep: Math.max(state.currentStep - 1, 0),
|
||||
};
|
||||
case "goToStep":
|
||||
return {
|
||||
...state,
|
||||
currentStep: Math.max(0, Math.min(action.step, TOTAL_STEPS - 1)),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export type QuestionType = "radio" | "text";
|
||||
|
||||
export interface BaseQuestion {
|
||||
id: string;
|
||||
type: QuestionType;
|
||||
question: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface RadioQuestion extends BaseQuestion {
|
||||
type: "radio";
|
||||
options: string[];
|
||||
}
|
||||
|
||||
export interface TextQuestion extends BaseQuestion {
|
||||
type: "text";
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export type SurveyQuestion = RadioQuestion | TextQuestion;
|
||||
|
||||
export interface SurveyState {
|
||||
currentStep: number;
|
||||
}
|
||||
|
||||
export type SurveyAction =
|
||||
| { type: "next" }
|
||||
| { type: "back" }
|
||||
| { type: "goToStep"; step: number };
|
||||
|
||||
export interface SurveyFormData {
|
||||
role?: string;
|
||||
signupReason?: string;
|
||||
referralSource?: string;
|
||||
}
|
||||
|
||||
export interface SurveyStepProps {
|
||||
question: SurveyQuestion;
|
||||
value: unknown;
|
||||
onChange: (value: unknown) => void;
|
||||
onNext?: () => void;
|
||||
showNext?: boolean;
|
||||
isLast?: boolean;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { useEffect } from "react";
|
||||
import type * as z from "zod/v4";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -9,12 +10,22 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormDescription,
|
||||
} from "@/src/components/ui/form";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/src/components/ui/select";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { organizationNameSchema } from "@/src/features/organizations/utils/organizationNameSchema";
|
||||
import { organizationFormSchema } from "@/src/features/organizations/utils/organizationNameSchema";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { SurveyName } from "@prisma/client";
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
export const NewOrganizationForm = ({
|
||||
onSuccess,
|
||||
@@ -24,23 +35,49 @@ export const NewOrganizationForm = ({
|
||||
const { update: updateSession } = useSession();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(organizationNameSchema),
|
||||
resolver: zodResolver(organizationFormSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
type: "Personal",
|
||||
size: undefined,
|
||||
},
|
||||
});
|
||||
const capture = usePostHogClientCapture();
|
||||
const createOrgMutation = api.organizations.create.useMutation({
|
||||
onError: (error) => form.setError("name", { message: error.message }),
|
||||
});
|
||||
const createSurveyMutation = api.surveys.create.useMutation();
|
||||
const watchedType = form.watch("type");
|
||||
const isCloud = Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION);
|
||||
|
||||
function onSubmit(values: z.infer<typeof organizationNameSchema>) {
|
||||
function onSubmit(values: z.infer<typeof organizationFormSchema>) {
|
||||
capture("organizations:new_form_submit");
|
||||
createOrgMutation
|
||||
.mutateAsync({
|
||||
name: values.name,
|
||||
})
|
||||
.then((org) => {
|
||||
.then(async (org) => {
|
||||
// Submit survey with organization data only on Cloud and if type is provided
|
||||
if (isCloud && values.type) {
|
||||
const surveyResponse: Record<string, string> = {
|
||||
type: values.type,
|
||||
};
|
||||
if (values.size) {
|
||||
surveyResponse.size = values.size;
|
||||
}
|
||||
|
||||
try {
|
||||
await createSurveyMutation.mutateAsync({
|
||||
surveyName: SurveyName.ORG_ONBOARDING,
|
||||
response: surveyResponse,
|
||||
orgId: org.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to submit survey:", error);
|
||||
// Continue with organization creation even if survey fails
|
||||
}
|
||||
}
|
||||
|
||||
void updateSession();
|
||||
onSuccess(org.id);
|
||||
form.reset();
|
||||
@@ -50,6 +87,13 @@ export const NewOrganizationForm = ({
|
||||
});
|
||||
}
|
||||
|
||||
// Clear size whenever type is not Company or Agency to avoid submitting hidden values
|
||||
useEffect(() => {
|
||||
if (watchedType !== "Company" && watchedType !== "Agency") {
|
||||
form.setValue("size", undefined);
|
||||
}
|
||||
}, [watchedType, form]);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -75,6 +119,69 @@ export const NewOrganizationForm = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{isCloud && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Type</FormLabel>
|
||||
<FormDescription>
|
||||
What would best describe your organization?
|
||||
</FormDescription>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger ref={field.ref}>
|
||||
<SelectValue placeholder="Please choose" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="Personal">Personal</SelectItem>
|
||||
<SelectItem value="Educational">Educational</SelectItem>
|
||||
<SelectItem value="Company">Company</SelectItem>
|
||||
<SelectItem value="Startup">Startup</SelectItem>
|
||||
<SelectItem value="Agency">Agency</SelectItem>
|
||||
<SelectItem value="N/A">N/A</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{(watchedType === "Company" || watchedType === "Agency") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="size"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{watchedType} size</FormLabel>
|
||||
<FormDescription>
|
||||
How many people are in your {watchedType}?
|
||||
</FormDescription>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger ref={field.ref}>
|
||||
<SelectValue placeholder="Please choose" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="1-10">1-10</SelectItem>
|
||||
<SelectItem value="10-49">10-49</SelectItem>
|
||||
<SelectItem value="50-99">50-99</SelectItem>
|
||||
<SelectItem value="100-299">100-299</SelectItem>
|
||||
<SelectItem value="More than 300">
|
||||
More than 300
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button type="submit" loading={createOrgMutation.isLoading}>
|
||||
Create
|
||||
</Button>
|
||||
|
||||
@@ -1,9 +1,46 @@
|
||||
import { StringNoHTML } from "@langfuse/shared";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
const organizationTypeOptions = [
|
||||
"Personal",
|
||||
"Educational",
|
||||
"Company",
|
||||
"Startup",
|
||||
"Agency",
|
||||
"N/A",
|
||||
] as const;
|
||||
|
||||
const organizationSizeOptions = [
|
||||
"1-10",
|
||||
"10-49",
|
||||
"50-99",
|
||||
"100-299",
|
||||
"More than 300",
|
||||
] as const;
|
||||
|
||||
// Base schema for org creation, used for server-side validation too
|
||||
export const organizationNameSchema = z.object({
|
||||
name: StringNoHTML.min(3, "Must have at least 3 characters").max(
|
||||
60,
|
||||
"Must have at most 60 characters",
|
||||
),
|
||||
});
|
||||
|
||||
// Extended schema for client-side form validation including type and size,
|
||||
// which are posted separately as a survey response.
|
||||
export const organizationFormSchema = organizationNameSchema
|
||||
.extend({
|
||||
type: z.enum(organizationTypeOptions),
|
||||
size: z.enum(organizationSizeOptions).optional(),
|
||||
})
|
||||
.check((ctx) => {
|
||||
const { type, size } = ctx.value;
|
||||
if ((type === "Company" || type === "Agency") && !size) {
|
||||
ctx.issues.push({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["size"],
|
||||
input: ctx.value.size,
|
||||
message: "Please specify the size of your organization",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
import { ForbiddenError, ObservationLevel } from "@langfuse/shared";
|
||||
import {
|
||||
ForbiddenError,
|
||||
ObservationLevel,
|
||||
ObservationTypeDomain,
|
||||
} from "@langfuse/shared";
|
||||
import {
|
||||
type TraceEventType,
|
||||
type IngestionEventType,
|
||||
@@ -634,23 +638,30 @@ export class OtelIngestionProcessor {
|
||||
}),
|
||||
};
|
||||
|
||||
const observationType = attributes[
|
||||
LangfuseOtelSpanAttributes.OBSERVATION_TYPE
|
||||
] as string;
|
||||
const isGeneration =
|
||||
attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] ===
|
||||
"generation" ||
|
||||
observationType === "generation" ||
|
||||
Boolean(observation.model) ||
|
||||
("openinference.span.kind" in attributes &&
|
||||
attributes["openinference.span.kind"] === "LLM");
|
||||
|
||||
const isEvent =
|
||||
attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] === "event";
|
||||
const isKnownObservationType =
|
||||
observationType &&
|
||||
ObservationTypeDomain.safeParse(observationType.toUpperCase()).success;
|
||||
|
||||
const getIngestionEventType = (): string => {
|
||||
if (isGeneration) return "generation-create";
|
||||
if (isKnownObservationType) {
|
||||
return `${observationType.toLowerCase()}-create`;
|
||||
}
|
||||
return "span-create";
|
||||
};
|
||||
|
||||
return {
|
||||
id: randomUUID(),
|
||||
type: isGeneration
|
||||
? "generation-create"
|
||||
: isEvent
|
||||
? "event-create"
|
||||
: "span-create",
|
||||
type: getIngestionEventType(),
|
||||
timestamp: new Date().toISOString(),
|
||||
body: observation,
|
||||
} as unknown as IngestionEventType;
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
type PlaceholderMessage,
|
||||
isPlaceholder,
|
||||
PromptType,
|
||||
isGenerationLike,
|
||||
} from "@langfuse/shared";
|
||||
import {
|
||||
LANGGRAPH_NODE_TAG,
|
||||
@@ -478,7 +479,7 @@ const parseGeneration = (
|
||||
},
|
||||
modelToProviderMap: Record<string, string>,
|
||||
): PlaygroundCache => {
|
||||
if (generation.type !== "GENERATION") return null;
|
||||
if (!isGenerationLike(generation.type)) return null;
|
||||
|
||||
const isLangGraph = isLangGraphTrace(generation);
|
||||
const modelParams = parseModelParams(generation, modelToProviderMap);
|
||||
@@ -611,7 +612,7 @@ function parseModelParams(
|
||||
if (!modelParams) return;
|
||||
|
||||
modelParams[key as keyof typeof parsedParams.data] = {
|
||||
value,
|
||||
value: value as any,
|
||||
enabled: true,
|
||||
};
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user