From b5e11186518f49651abc43b59ad3427ca895b8c3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 9 Sep 2026 04:33:58 +0000 Subject: [PATCH 1/4] docs: use Fumadocs TypeTable on data model pages Convert Score/ScoreConfig and Experiments dataset object field tables to TypeTable, register the component for MDX, and keep the same fields in Markdown/PDF export. Co-authored-by: Marc Klingen --- .../evaluation/experiments/data-model.mdx | 198 ++++++++++++++---- content/docs/evaluation/scores/data-model.mdx | 134 +++++++++--- lib/markdown-component-renderers.js | 197 ++++++++++++++++- mdx-components.tsx | 2 + 4 files changed, 467 insertions(+), 64 deletions(-) diff --git a/content/docs/evaluation/experiments/data-model.mdx b/content/docs/evaluation/experiments/data-model.mdx index 9b16745870..5ee3d54556 100644 --- a/content/docs/evaluation/experiments/data-model.mdx +++ b/content/docs/evaluation/experiments/data-model.mdx @@ -59,39 +59,118 @@ direction LR #### Dataset object [#dataset-object] -| Attribute | Type | Required | Description | -| ------------------------- | ------ | -------- | ------------------------------------------- | -| `id` | string | Yes | Unique identifier for the dataset | -| `name` | string | Yes | Name of the dataset | -| `description` | string | No | Description of the dataset | -| `metadata` | object | No | Additional metadata for the dataset | -| `remoteExperimentUrl` | string | No | Webhook endpoint for triggering experiments | -| `remoteExperimentPayload` | object | No | Payload for triggering experiments | + #### DatasetItem object [#datasetitem-object] -| Attribute | Type | Required | Description | -| --------------------- | ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | string | Yes | Unique identifier for the dataset item. Dataset items are upserted on their id. Id needs to be unique (project-level) and cannot be reused across datasets. | -| `datasetId` | string | Yes | ID of the dataset this item belongs to | -| `input` | object | No | Input data for the dataset item | -| `expectedOutput` | object | No | Expected output data for the dataset item | -| `metadata` | object | No | Additional metadata for the dataset item | -| `mediaReferences` | object[] | No | Resolved media references found in `input`, `expectedOutput`, and `metadata`. Included on SDK dataset fetches and API responses that include resolved dataset media. | -| `sourceTraceId` | string | No | ID of the source trace to link this dataset item to | -| `sourceObservationId` | string | No | ID of the source observation to link this dataset item to | -| `status` | DatasetStatus | No | Status of the dataset item. Defaults to ACTIVE for newly created items. Possible values: `ACTIVE`, `ARCHIVED` | + #### DatasetItemMediaReference object [#datasetitemmediareference-object] Dataset item media references point from a stored media token in `input`, `expectedOutput`, or `metadata` to a signed media download URL. -| Attribute | Type | Required | Description | -| ----------------- | ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| `field` | string | Yes | Field enum for the dataset item property containing the reference. One of `input`, `expected_output` (for `expectedOutput`), or `metadata`. | -| `referenceString` | string | Yes | Original Langfuse media reference string stored in the dataset item. | -| `jsonPath` | string | Yes | JSONPath of the string holding the reference inside the field, for example `$['image']`. | -| `media` | object | Yes (nullable) | Resolved media metadata. `null` if the referenced media does not exist or has not been uploaded successfully. | + The nested `media` object contains `mediaId`, `contentType`, `contentLength`, `url`, and `urlExpiry`. The `url` is a signed download URL and should be used before its expiration date. To refresh the signed URL, refetch the dataset. @@ -125,23 +204,64 @@ direction LR #### DatasetRun object [#datasetrun-object] -| Attribute | Type | Required | Description | -| ------------- | ------ | -------- | --------------------------------------- | -| `id` | string | Yes | Unique identifier for the dataset run | -| `name` | string | Yes | Name of the dataset run | -| `description` | string | No | Description of the dataset run | -| `metadata` | object | No | Additional metadata for the dataset run | -| `datasetId` | string | Yes | ID of the dataset this run belongs to | + #### DatasetRunItem object [#datasetrunitem-object] -| Attribute | Type | Required | Description | -| --------------- | ------ | -------- | ------------------------------------------ | -| `id` | string | Yes | Unique identifier for the dataset run item | -| `datasetRunId` | string | Yes | ID of the dataset run this item belongs to | -| `datasetItemId` | string | Yes | ID of the dataset item to link to this run | -| `traceId` | string | Yes | ID of the trace to link to this run | -| `observationId` | string | No | ID of the observation to link to this run | + Langfuse currently assumes that experiments do not contain repetitions: each dataset item appears once per experiment. Accordingly, reads surface at most one experiment item per dataset item within an experiment. Repetition support is tracked in [#5855](https://github.com/langfuse/langfuse/issues/5855). diff --git a/content/docs/evaluation/scores/data-model.mdx b/content/docs/evaluation/scores/data-model.mdx index 5cc7875310..6651de22b7 100644 --- a/content/docs/evaluation/scores/data-model.mdx +++ b/content/docs/evaluation/scores/data-model.mdx @@ -48,20 +48,70 @@ Scores have the following properties: ### Score object [#score-object] -| Attribute | Type | Required | Description | -| --------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | string | Yes | Unique identifier of the score. Auto-generated by SDKs. Optionally can also be used as an idempotency key to update scores. | -| `name` | string | Yes | Name of the score, e.g. user_feedback, hallucination_eval | -| `value` | number | No | Numeric value of the score. Always defined for numeric and boolean scores. Optional for categorical scores. Not used for text scores. | -| `stringValue` | string | No | String value of the score. Used for categorical, boolean (string equivalent), and text data types. Automatically set for categorical scores based on the config if the `configId` is provided. | -| `dataType` | string | No | Automatically set based on the config data type when the `configId` is provided. Otherwise can be defined manually as `NUMERIC`, `CATEGORICAL`, `BOOLEAN`, or `TEXT` | -| `source` | string | Yes | Automatically set based on the source of the score. Can be either `API`, `EVAL`, or `ANNOTATION` | -| `comment` | string | No | Evaluation comment, commonly used for user feedback, eval reasoning output or internal notes | -| `traceId` | string | No | Id of the trace the score relates to | -| `observationId` | string | No | Id of the observation (e.g. LLM call) the score relates to | -| `sessionId` | string | No | Id of the session the score relates to | -| `datasetRunId` | string | No | Id of the dataset run the score relates to | -| `configId` | string | No | Score config id to ensure that the score follows a specific schema. Can be defined in the Langfuse UI or via API. | + ### Common Use Cases [#common-use-cases] @@ -91,13 +141,49 @@ A score config includes: ### ScoreConfig object [#scoreconfig-object] -| Attribute | Type | Required | Description | -| ------------- | ------- | -------- | ------------------------------------------------------------------------------------- | -| `id` | string | Yes | Unique identifier of the score config. | -| `name` | string | Yes | Name of the score config, e.g. user_feedback, hallucination_eval | -| `dataType` | string | Yes | Can be either `NUMERIC`, `CATEGORICAL`, `BOOLEAN`, or `TEXT` | -| `isArchived` | boolean | No | Whether the score config is archived. Defaults to false | -| `minValue` | number | No | Sets minimum value for numerical scores. If not set, the minimum value defaults to -∞ | -| `maxValue` | number | No | Sets maximum value for numerical scores. If not set, the maximum value defaults to +∞ | -| `categories` | list | No | Defines categories for categorical scores. List of objects with label value pairs | -| `description` | string | No | Provides further description of the score configuration | + diff --git a/lib/markdown-component-renderers.js b/lib/markdown-component-renderers.js index 4010165d20..f3a1cec51a 100644 --- a/lib/markdown-component-renderers.js +++ b/lib/markdown-component-renderers.js @@ -36,7 +36,9 @@ const CODING_AGENTS_SPEND_SUMMARY = function replaceComponentsWithMarkdown(fileContent) { const exportedResourceArrays = extractExportedResourceArrays(fileContent); return stripResourceArrayExports( - replaceCardGroupsWithMarkdown(replaceAcademyComponents(fileContent)), + replaceCardGroupsWithMarkdown( + replaceAcademyComponents(replaceTypeTablesWithMarkdown(fileContent)), + ), Object.keys(exportedResourceArrays), ) .replace( @@ -159,6 +161,199 @@ function replaceComponentsWithMarkdown(fileContent) { ); } +function replaceTypeTablesWithMarkdown(fileContent) { + return replaceSelfClosingComponents( + fileContent, + ["TypeTable"], + (_, attributes) => renderTypeTable(attributes), + ); +} + +/** + * Turns Fumadocs TypeTable JSX into a Markdown field table so .md / PDF / + * md-src output keeps the same attribute names, types, and descriptions. + */ +function renderTypeTable(attributes) { + const typeExpr = extractJsxExpression(attributes, "type"); + if (!typeExpr) { + throw new Error("TypeTable requires a type object"); + } + + const trimmed = typeExpr.trim(); + if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) { + throw new Error("TypeTable type must be an object literal"); + } + + const entries = parseTypeTableEntries(trimmed.slice(1, -1)); + if (entries.length === 0) { + throw new Error("TypeTable type object is empty"); + } + + const includeDefault = entries.some((entry) => entry.default != null); + const includeDeprecated = entries.some((entry) => entry.deprecated); + + const headers = ["Attribute", "Type", "Required"]; + if (includeDefault) headers.push("Default"); + headers.push("Description"); + + const lines = [ + `| ${headers.join(" | ")} |`, + `| ${headers.map(() => "---").join(" | ")} |`, + ]; + + for (const entry of entries) { + const cells = [ + `\`${escapeTableCell(entry.name)}\``, + entry.type ? `\`${escapeTableCell(entry.type)}\`` : "", + entry.required ? "Yes" : "No", + ]; + if (includeDefault) { + cells.push( + entry.default != null ? `\`${escapeTableCell(entry.default)}\`` : "", + ); + } + + let description = entry.description ?? ""; + if (includeDeprecated && entry.deprecated) { + description = description ? `${description} Deprecated.` : "Deprecated."; + } + cells.push(escapeTableCell(description)); + lines.push(`| ${cells.join(" | ")} |`); + } + + return lines.join("\n"); +} + +/** Reads `prop={...}` and returns the expression source inside the braces. */ +function extractJsxExpression(attributes, propName) { + const start = attributes.search(new RegExp("\\b" + propName + "=\\{")); + if (start === -1) return null; + const open = attributes.indexOf("{", start + propName.length); + if (open === -1) return null; + + let depth = 0; + let quote = null; + for (let i = open; i < attributes.length; i++) { + const ch = attributes[i]; + if (quote) { + if (ch === "\\") i++; + else if (ch === quote) quote = null; + continue; + } + if (ch === '"' || ch === "'" || ch === "`") { + quote = ch; + continue; + } + if (ch === "{") depth++; + else if (ch === "}") { + depth--; + if (depth === 0) return attributes.slice(open + 1, i); + } + } + return null; +} + +/** + * Parses `fieldName: { type, description, ... }` entries from a TypeTable + * type-object body. + */ +function parseTypeTableEntries(typeBody) { + const entries = []; + let i = 0; + + while (i < typeBody.length) { + while (i < typeBody.length && /[\s,]/.test(typeBody[i])) i++; + if (i >= typeBody.length) break; + + let name; + if (typeBody[i] === '"' || typeBody[i] === "'") { + const quote = typeBody[i]; + let j = i + 1; + while (j < typeBody.length && typeBody[j] !== quote) { + if (typeBody[j] === "\\") j++; + j++; + } + name = typeBody.slice(i + 1, j); + i = j + 1; + } else { + const match = typeBody.slice(i).match(/^[A-Za-z_$][\w$]*/); + if (!match) break; + name = match[0]; + i += name.length; + } + + while (i < typeBody.length && /\s/.test(typeBody[i])) i++; + if (typeBody[i] !== ":") { + throw new Error(`TypeTable field "${name}" is missing a value`); + } + i++; + while (i < typeBody.length && /\s/.test(typeBody[i])) i++; + if (typeBody[i] !== "{") { + throw new Error(`TypeTable field "${name}" must be an object`); + } + + let depth = 0; + let quote = null; + const objectStart = i; + let objectEnd = -1; + for (; i < typeBody.length; i++) { + const ch = typeBody[i]; + if (quote) { + if (ch === "\\") { + i++; + continue; + } + if (ch === quote) quote = null; + continue; + } + if (ch === '"' || ch === "'" || ch === "`") { + quote = ch; + continue; + } + if (ch === "{") depth++; + else if (ch === "}") { + depth--; + if (depth === 0) { + objectEnd = i; + i++; + break; + } + } + } + + if (objectEnd === -1) { + throw new Error(`TypeTable field "${name}" has an unclosed object`); + } + + const source = typeBody.slice(objectStart + 1, objectEnd); + entries.push({ + name, + type: extractTypeTableValue(source, "type"), + description: extractTypeTableValue(source, "description"), + default: extractTypeTableValue(source, "default"), + required: /\brequired:\s*true\b/.test(source), + deprecated: /\bdeprecated:\s*true\b/.test(source), + }); + } + + return entries; +} + +function extractTypeTableValue(source, property) { + const node = extractObjectNodeText(source, property); + if (node != null) return node; + + const booleanMatch = source.match( + new RegExp("\\b" + property + ":\\s*(true|false)\\b"), + ); + if (booleanMatch) return booleanMatch[1]; + + const numberMatch = source.match( + new RegExp("\\b" + property + ":\\s*(-?\\d+(?:\\.\\d+)?)"), + ); + return numberMatch ? numberMatch[1] : null; +} + function renderSelfHostScaleMetrics() { return `Langfuse OSS and Enterprise use the same codebase as Langfuse Cloud. Langfuse processes **${formatObservationsPerMonth()} observations per month** and is trusted by **${FORTUNE_50_COMPANIES} of the Fortune 50**. Its Docker images have been pulled **${formatDockerPulls()} times**.`; } diff --git a/mdx-components.tsx b/mdx-components.tsx index 1a134ab756..abcf43c34d 100644 --- a/mdx-components.tsx +++ b/mdx-components.tsx @@ -1,4 +1,5 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; +import { TypeTable } from "fumadocs-ui/components/type-table"; import type { MDXComponents } from "mdx/types"; import React from "react"; import dynamic from "next/dynamic"; @@ -109,6 +110,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents { Tabs: LangTabsWithTab, Tab: LangTab, table: Table, + TypeTable, Cards, Card, "Cards.Card": Card, From 8ab4f147634945cdc51acc7c5bd20112a313a7f6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 11 Sep 2026 22:42:15 +0000 Subject: [PATCH 2/4] style: restyle Fumadocs TypeTable to match docs chrome Add three switchable TypeTable variants (flat table default, CornerBox chrome, always-visible rows) and drop the Fumadocs rounded card. Toggle with ?typetable=1|2|3. Co-authored-by: Marc Klingen --- app/layout.tsx | 7 + components/docs/type-table.tsx | 76 +++++++++ mdx-components.tsx | 2 +- src/overrides.css | 276 +++++++++++++++++++++++++++++++++ 4 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 components/docs/type-table.tsx diff --git a/app/layout.tsx b/app/layout.tsx index 7d93f48bc2..a0f6ba2ac1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -83,6 +83,13 @@ export default function RootLayout({ className={`${interVariable.variable} ${geistMono.variable} ${f37Analog.variable}`} > +