Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba91ed14f1 | ||
|
|
49e8c8c5e0 | ||
|
|
5748cfedd4 | ||
|
|
3f6916b13f | ||
|
|
d7a4a34df1 | ||
|
|
3d3e8967a6 | ||
|
|
f1bf562811 | ||
|
|
02d6c78626 | ||
|
|
251741aacb | ||
|
|
ce49294607 | ||
|
|
a0c876271a | ||
|
|
221d8e9043 | ||
|
|
e5e30eb4cb | ||
|
|
7834a37d32 | ||
|
|
261d857e90 | ||
|
|
25889323e6 | ||
|
|
14f766908b | ||
|
|
6f06a3ab5f | ||
|
|
f706721c12 | ||
|
|
35c8adaccc | ||
|
|
465e1661fa | ||
|
|
1cf917edde | ||
|
|
80efe21420 | ||
|
|
6be2e203ba | ||
|
|
488f5494e0 | ||
|
|
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 |
@@ -90,4 +90,7 @@ LANGFUSE_EXPERIMENT_COMPARE_READ_FROM_AGGREGATING_MERGE_TREES="true"
|
||||
LANGFUSE_EXPERIMENT_WHITELISTED_PROJECT_IDS="7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
|
||||
LANGFUSE_EXPERIMENT_ADD_QUERY_RESULT_TO_SPAN_PROJECT_IDS="7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
|
||||
LANGFUSE_EXPERIMENT_RETURN_NEW_RESULT="true"
|
||||
LANGFUSE_EXPERIMENT_SAMPLING_RATE=1
|
||||
LANGFUSE_EXPERIMENT_SAMPLING_RATE=1
|
||||
|
||||
# LANGFUSE_EXPERIMENT_DATASET_RUN_ITEMS_WRITE_CH=true
|
||||
# LANGFUSE_EXPERIMENT_DATASET_RUN_ITEMS_READ_CH=true
|
||||
|
||||
@@ -1,33 +1,45 @@
|
||||
name: 🐞 Bug Report
|
||||
description: Create a bug report to help us improve
|
||||
title: "bug: "
|
||||
description: Report a bug to help us improve
|
||||
title: "bug: <short description>"
|
||||
labels: ["🐞❔ unconfirmed bug"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.
|
||||
description: A clear and concise description of the bug, and what you expected to happen when you encountered it.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To reproduce
|
||||
description: Describe how to reproduce your bug. Please provide detailed steps, code snippets, reproduction repos etc.
|
||||
label: Steps to reproduce
|
||||
description: Describe how to reproduce the bug. Please provide detailed steps, code snippets, a minimal reproduction repository, etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Langfuse Cloud or self-hosted?
|
||||
options:
|
||||
- "Langfuse Cloud"
|
||||
- "Self-hosted"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: If self-hosted, what version are you running?
|
||||
description: We may ask you to upgrade to the latest version, as many issues are continuously being fixed.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: SDK and container versions
|
||||
description: If you're experiencing an issue with an integration or SDK, please ensure you're using the latest version. If you're self-hosting Langfuse, check that you're running the most recent version. If updating isn't an option, please provide the specific versions you're currently using.
|
||||
label: SDK and integration versions
|
||||
description: If you're experiencing an issue with an integration or SDK, please share all package versions you're using. If you are not on the latest version, try upgrading, as this will often resolve the issue.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the bug here, screenshots if applicable.
|
||||
description: Add any other information related to the bug here, including screenshots if applicable.
|
||||
- type: dropdown
|
||||
id: contribute
|
||||
attributes:
|
||||
label: Are you interested to contribute a fix for this bug?
|
||||
description: If this is a confirmed bug, the maintainers are happy to support with guidance and review.
|
||||
label: Are you interested in contributing a fix for this bug?
|
||||
description: If this is a confirmed bug, the maintainers are happy to provide guidance and review.
|
||||
options:
|
||||
- "No"
|
||||
- "Yes"
|
||||
|
||||
@@ -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:
|
||||
|
||||
+88
-86
@@ -91,12 +91,11 @@ Langfuse 是一个 **开源 LLM 工程** 平台。它帮助团队协作 **开发
|
||||

|
||||
|
||||
- [LLM 应用可观察性](https://langfuse.com/docs/tracing):为你的应用插入仪表代码,并开始将追踪数据传送到 Langfuse,从而追踪 LLM 调用及应用中其他相关逻辑(如检索、嵌入或代理操作)。检查并调试复杂日志及用户会话。试试互动的 [演示](https://langfuse.com/docs/demo) 看看效果。
|
||||
|
||||
- [提示管理](https://langfuse.com/docs/prompts/get-started) 帮助你集中管理、版本控制并协作迭代提示。得益于服务器和客户端的高效缓存,你可以在不增加延迟的情况下反复迭代提示。
|
||||
- [提示管理](https://langfuse.com/docs/prompt-management/get-started) 帮助你集中管理、版本控制并协作迭代提示。得益于服务器和客户端的高效缓存,你可以在不增加延迟的情况下反复迭代提示。
|
||||
|
||||
- [评估](https://langfuse.com/docs/scores/overview) 是 LLM 应用开发流程的关键组成部分,Langfuse 能够满足你的多样需求。它支持 LLM 作为"裁判"、用户反馈收集、手动标注以及通过 API/SDK 实现自定义评估流程。
|
||||
- [评估](https://langfuse.com/docs/evaluation/overview) 是 LLM 应用开发流程的关键组成部分,Langfuse 能够满足你的多样需求。它支持 LLM 作为"裁判"、用户反馈收集、手动标注以及通过 API/SDK 实现自定义评估流程。
|
||||
|
||||
- [数据集](https://langfuse.com/docs/datasets/overview) 为评估你的 LLM 应用提供测试集和基准。它们支持持续改进、部署前测试、结构化实验、灵活评估,并能与 LangChain、LlamaIndex 等框架无缝整合。
|
||||
- [数据集](https://langfuse.com/docs/evaluation/dataset-runs/datasets) 为评估你的 LLM 应用提供测试集和基准。它们支持持续改进、部署前测试、结构化实验、灵活评估,并能与 LangChain、LlamaIndex 等框架无缝整合。
|
||||
|
||||
- [LLM 试玩平台](https://langfuse.com/docs/playground) 是用于测试和迭代提示及模型配置的工具,缩短反馈周期,加速开发。当你在追踪中发现异常结果时,可以直接跳转至试玩平台进行调整。
|
||||
|
||||
@@ -122,14 +121,14 @@ Langfuse 是一个 **开源 LLM 工程** 平台。它帮助团队协作 **开发
|
||||
|
||||
- [本地(docker compose)](https://langfuse.com/self-hosting/local):使用 Docker Compose 在你的机器上于 5 分钟内运行 Langfuse。
|
||||
|
||||
````bash:README.md/docker-compose
|
||||
```bash:README.md/docker-compose
|
||||
# 获取最新的 Langfuse 仓库副本
|
||||
git clone https://github.com/langfuse/langfuse.git
|
||||
cd langfuse
|
||||
|
||||
# 运行 Langfuse 的 docker compose
|
||||
docker compose up
|
||||
`````
|
||||
```
|
||||
|
||||
- [Kubernetes(Helm)](https://langfuse.com/self-hosting/kubernetes-helm):使用 Helm 在 Kubernetes 集群上部署 Langfuse。这是推荐的生产环境部署方式。
|
||||
|
||||
@@ -145,40 +144,40 @@ Langfuse 是一个 **开源 LLM 工程** 平台。它帮助团队协作 **开发
|
||||
|
||||
### 主要集成:
|
||||
|
||||
| 集成 | 支持语言/平台 | 描述 |
|
||||
| ---------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | 使用 SDK 进行手动仪表化,实现全面灵活性。 |
|
||||
| [OpenAI](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | 通过直接替换 OpenAI SDK 实现自动仪表化。 |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | 通过传入回调处理器至 Langchain 应用实现自动仪表化。 |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | 通过 LlamaIndex 回调系统实现自动仪表化。 |
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | 通过 Haystack 内容追踪系统实现自动仪表化。 |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (仅代理) | 允许使用任何 LLM 替代 GPT。支持 Azure、OpenAI、Cohere、Anthropic、Ollama、VLLM、Sagemaker、HuggingFace、Replicate(100+ LLMs)。 |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | 基于 TypeScript 的工具包,帮助开发者使用 React、Next.js、Vue、Svelte 和 Node.js 构建 AI 驱动的应用。 |
|
||||
| [API](https://langfuse.com/docs/api) | | 直接调用公共 API。提供 OpenAPI 规格。 |
|
||||
| [Google VertexAI 和 Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | 模型 | 在 Google 上运行基础模型和微调模型。 |
|
||||
| 集成 | 支持语言/平台 | 描述 |
|
||||
| ------------------------------------------------------------------------------------ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | 使用 SDK 进行手动仪表化,实现全面灵活性。 |
|
||||
| [OpenAI](https://langfuse.com/integrations/model-providers/openai-py) | Python, JS/TS | 通过直接替换 OpenAI SDK 实现自动仪表化。 |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | 通过传入回调处理器至 Langchain 应用实现自动仪表化。 |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | 通过 LlamaIndex 回调系统实现自动仪表化。 |
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | 通过 Haystack 内容追踪系统实现自动仪表化。 |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (仅代理) | 允许使用任何 LLM 替代 GPT。支持 Azure、OpenAI、Cohere、Anthropic、Ollama、VLLM、Sagemaker、HuggingFace、Replicate(100+ LLMs)。 |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | 基于 TypeScript 的工具包,帮助开发者使用 React、Next.js、Vue、Svelte 和 Node.js 构建 AI 驱动的应用。 |
|
||||
| [API](https://langfuse.com/docs/api) | | 直接调用公共 API。提供 OpenAPI 规格。 |
|
||||
| [Google VertexAI 和 Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | 模型 | 在 Google 上运行基础模型和微调模型。 |
|
||||
|
||||
### 与 Langfuse 集成的软件包:
|
||||
|
||||
| 名称 | 类型 | 描述 |
|
||||
| ---------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| [Instructor](https://langfuse.com/docs/integrations/instructor) | 库 | 用于获取结构化 LLM 输出(JSON、Pydantic)的库。 |
|
||||
| [DSPy](https://langfuse.com/docs/integrations/dspy) | 库 | 一个系统性优化语言模型提示和权重的框架。 |
|
||||
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | 模型 | 在 AWS 上运行基础模型和微调模型。 |
|
||||
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | 库 | 构建 LLM 应用的 Python 工具包。 |
|
||||
| [Ollama](https://langfuse.com/docs/integrations/ollama) | 模型(本地) | 在你的机器上轻松运行开源 LLM。 |
|
||||
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | 代理框架 | 用于构建分布式代理的开源 LLM 平台。 |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | 聊天/代理界面 | 基于 JS/TS 的无代码构建器,用于定制化 LLM 流程。 |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | 聊天/代理界面 | 基于 Python 的 LangChain 用户界面,采用 react-flow 设计,提供便捷的实验与原型构建体验。 |
|
||||
| [Dify](https://langfuse.com/docs/integrations/dify) | 聊天/代理界面 | 带有无代码构建器的开源 LLM 应用开发平台。 |
|
||||
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | 聊天/代理界面 | 自托管的 LLM 聊天网页界面,支持包括自托管和本地模型在内的多种 LLM 运行器。 |
|
||||
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | 工具 | 开源 LLM 测试平台。 |
|
||||
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | 聊天/代理界面 | 开源聊天机器人平台。 |
|
||||
| [Vapi](https://langfuse.com/docs/integrations/vapi) | 平台 | 开源语音 AI 平台。 |
|
||||
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | 代理 | 构建分布式代理的开源 LLM 平台。 |
|
||||
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | 聊天/代理界面 | 开源 Python 库,可用于构建类似聊天 UI 的网页界面。 |
|
||||
| [Goose](https://langfuse.com/docs/integrations/goose) | 代理 | 构建分布式代理的开源 LLM 平台。 |
|
||||
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | 代理 | 开源 AI 代理框架。 |
|
||||
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | 代理 | 多代理框架,用于实现代理之间的协作与工具调用。 |
|
||||
| 名称 | 类型 | 描述 |
|
||||
| ----------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------- |
|
||||
| [Instructor](https://langfuse.com/docs/integrations/instructor) | 库 | 用于获取结构化 LLM 输出(JSON、Pydantic)的库。 |
|
||||
| [DSPy](https://langfuse.com/docs/integrations/dspy) | 库 | 一个系统性优化语言模型提示和权重的框架。 |
|
||||
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | 模型 | 在 AWS 上运行基础模型和微调模型。 |
|
||||
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | 库 | 构建 LLM 应用的 Python 工具包。 |
|
||||
| [Ollama](https://langfuse.com/docs/integrations/ollama) | 模型(本地) | 在你的机器上轻松运行开源 LLM。 |
|
||||
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | 代理框架 | 用于构建分布式代理的开源 LLM 平台。 |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | 聊天/代理界面 | 基于 JS/TS 的无代码构建器,用于定制化 LLM 流程。 |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | 聊天/代理界面 | 基于 Python 的 LangChain 用户界面,采用 react-flow 设计,提供便捷的实验与原型构建体验。 |
|
||||
| [Dify](https://langfuse.com/docs/integrations/dify) | 聊天/代理界面 | 带有无代码构建器的开源 LLM 应用开发平台。 |
|
||||
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | 聊天/代理界面 | 自托管的 LLM 聊天网页界面,支持包括自托管和本地模型在内的多种 LLM 运行器。 |
|
||||
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | 工具 | 开源 LLM 测试平台。 |
|
||||
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | 聊天/代理界面 | 开源聊天机器人平台。 |
|
||||
| [Vapi](https://langfuse.com/docs/integrations/vapi) | 平台 | 开源语音 AI 平台。 |
|
||||
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | 代理 | 构建分布式代理的开源 LLM 平台。 |
|
||||
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | 聊天/代理界面 | 开源 Python 库,可用于构建类似聊天 UI 的网页界面。 |
|
||||
| [Goose](https://langfuse.com/docs/integrations/goose) | 代理 | 构建分布式代理的开源 LLM 平台。 |
|
||||
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | 代理 | 开源 AI 代理框架。 |
|
||||
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | 代理 | 多代理框架,用于实现代理之间的协作与工具调用。 |
|
||||
|
||||
## 🚀 快速入门
|
||||
|
||||
@@ -192,25 +191,25 @@ Langfuse 是一个 **开源 LLM 工程** 平台。它帮助团队协作 **开发
|
||||
|
||||
### 2️⃣ 记录你的第一个 LLM 调用
|
||||
|
||||
使用 [<code>@observe()</code> 装饰器](https://langfuse.com/docs/sdk/python/decorators) 可轻松跟踪任何 Python LLM 应用。在本快速入门中,我们还使用了 Langfuse 的 [OpenAI 集成](https://langfuse.com/docs/integrations/openai) 来自动捕获所有模型参数。
|
||||
使用 [<code>@observe()</code> 装饰器](https://langfuse.com/docs/sdk/python/decorators) 可轻松跟踪任何 Python LLM 应用。在本快速入门中,我们还使用了 Langfuse 的 [OpenAI 集成](https://langfuse.com/integrations/model-providers/openai-py) 来自动捕获所有模型参数。
|
||||
|
||||
> [!提示]
|
||||
> 不使用 OpenAI?请访问 [我们的文档](https://langfuse.com/docs/get-started#log-your-first-llm-call-to-langfuse) 了解如何记录其他模型和框架。
|
||||
|
||||
安装依赖:
|
||||
|
||||
````bash
|
||||
```bash
|
||||
pip install langfuse openai
|
||||
````
|
||||
```
|
||||
|
||||
配置环境变量(创建名为 **.env** 的文件):
|
||||
|
||||
````bash:.env
|
||||
```bash:.env
|
||||
LANGFUSE_SECRET_KEY="sk-lf-..."
|
||||
LANGFUSE_PUBLIC_KEY="pk-lf-..."
|
||||
LANGFUSE_HOST="https://cloud.langfuse.com" # 🇪🇺 欧盟区域
|
||||
# LANGFUSE_HOST="https://us.cloud.langfuse.com" # 🇺🇸 美洲区域
|
||||
````
|
||||
```
|
||||
|
||||
创建示例代码(文件名:**main.py**):
|
||||
|
||||
@@ -230,7 +229,7 @@ def main():
|
||||
return story()
|
||||
|
||||
main()
|
||||
````
|
||||
```
|
||||
|
||||
### 3️⃣ 在 Langfuse 中查看追踪记录
|
||||
|
||||
@@ -289,51 +288,51 @@ _[Langfuse 中的公共示例追踪](https://cloud.langfuse.com/project/cloramnk
|
||||
|
||||
以下是使用 Langfuse 的顶级开源 Python 项目,按星标数排名([来源](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents)):
|
||||
|
||||
| 仓库 | 星数 |
|
||||
| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----: |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
|
||||
| 仓库 | 星数 |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----: |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/119600397?s=40&v=4" width="20" height="20" alt=""> [twentyhq](https://github.com/twentyhq) / [twenty](https://github.com/twentyhq/twenty) | 24195 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22618 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 15151 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179202840?s=40&v=4" width="20" height="20" alt=""> [mediar-ai](https://github.com/mediar-ai) / [screenpipe](https://github.com/mediar-ai/screenpipe) | 11037 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105877416?s=40&v=4" width="20" height="20" alt=""> [formbricks](https://github.com/formbricks) / [formbricks](https://github.com/formbricks/formbricks) | 9386 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 8385 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7374 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5391 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 4976 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 4141 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2526 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2061 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1161 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1100 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1074 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 971 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 823 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 677 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 639 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 514 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 394 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 384 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 370 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 305 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/154247157?s=40&v=4" width="20" height="20" alt=""> [vespperhq](https://github.com/vespperhq) / [vespper](https://github.com/vespperhq/vespper) | 304 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> [block](https://github.com/block) / [goose](https://github.com/block/goose) | 295 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 291 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 221 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 172 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 148 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/119600397?s=40&v=4" width="20" height="20" alt=""> [twentyhq](https://github.com/twentyhq) / [twenty](https://github.com/twentyhq/twenty) | 24195 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22618 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 15151 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179202840?s=40&v=4" width="20" height="20" alt=""> [mediar-ai](https://github.com/mediar-ai) / [screenpipe](https://github.com/mediar-ai/screenpipe) | 11037 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105877416?s=40&v=4" width="20" height="20" alt=""> [formbricks](https://github.com/formbricks) / [formbricks](https://github.com/formbricks/formbricks) | 9386 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 8385 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7374 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5391 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 4976 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 4141 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2526 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2061 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1161 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1100 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1074 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 971 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 823 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 677 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 639 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 514 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 394 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 384 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 370 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 305 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/154247157?s=40&v=4" width="20" height="20" alt=""> [vespperhq](https://github.com/vespperhq) / [vespper](https://github.com/vespperhq/vespper) | 304 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> [block](https://github.com/block) / [goose](https://github.com/block/goose) | 295 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 291 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 221 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 172 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 148 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
|
||||
|
||||
## 🔒 安全与隐私
|
||||
@@ -352,4 +351,7 @@ _[Langfuse 中的公共示例追踪](https://cloud.langfuse.com/project/cloramnk
|
||||
所有数据均不会与第三方共享,也不包含任何敏感信息。我们对这一过程保持高度透明,你可以在 [此处](/web/src/features/telemetry/index.ts) 查看我们收集的具体数据。
|
||||
|
||||
你可以通过设置 `TELEMETRY_ENABLED=false` 来选择退出。
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
+80
-80
@@ -84,15 +84,15 @@ Langfuseは**数分でセルフホスト可能**で、**多くの実績を持つ
|
||||
複雑なログやユーザーセッションを解析・デバッグできます。
|
||||
インタラクティブな[デモ](https://langfuse.com/docs/demo)で動作を確認してください。
|
||||
|
||||
- **[プロンプト管理](https://langfuse.com/docs/prompts/get-started):**
|
||||
- **[プロンプト管理](https://langfuse.com/docs/prompt-management/get-started):**
|
||||
プロンプトを一元管理し、バージョン管理しながら共同で改善を行えます。
|
||||
サーバーおよびクライアント側で強力なキャッシングを行うため、アプリケーションのレイテンシを増やすことなくプロンプトの改良が可能です。
|
||||
|
||||
- **[評価](https://langfuse.com/docs/scores/overview):**
|
||||
- **[評価](https://langfuse.com/docs/evaluation/overview):**
|
||||
評価はLLMアプリケーション開発ワークフローの要であり、Langfuseは多様なニーズに対応します。
|
||||
LLMを判定者として用いる方法、ユーザーフィードバックの収集、手動によるラベリング、API/SDKを通じたカスタム評価パイプラインをサポートします。
|
||||
|
||||
- **[データセット](https://langfuse.com/docs/datasets/overview):**
|
||||
- **[データセット](https://langfuse.com/docs/evaluation/dataset-runs/datasets):**
|
||||
LLMアプリケーション評価用のテストセットやベンチマークを構築できます。
|
||||
継続的な改善、事前デプロイテスト、構造化された実験、柔軟な評価、さらにLangChainやLlamaIndexなどとのシームレスな統合をサポートします。
|
||||
|
||||
@@ -151,40 +151,40 @@ Langfuseチームによるマネージドデプロイメント。充実した無
|
||||
|
||||
### 主なインテグレーション:
|
||||
|
||||
| インテグレーション | 対応言語・環境 | 説明 |
|
||||
| ----------------------------------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | SDKを利用して手動でインストゥルメンテーションを実装し、完全な柔軟性を提供します。 |
|
||||
| [OpenAI](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | OpenAI SDKのドロップイン置換による自動インストゥルメンテーションを実現します。 |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Langchainアプリケーションにコールバックハンドラーを渡すことで自動的に計測します。 |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | LlamaIndexのコールバックシステムを介して自動的にインストゥルメントします。 |
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Haystackのコンテンツトレースシステムを利用した自動インストゥルメンテーションを実現します。 |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only)| GPTのドロップイン置換として任意のLLMを使用できます。Azure、OpenAI、Cohere、Anthropic、Ollama、VLLM、Sagemaker、HuggingFace、Replicate(100+ LLM)に対応。 |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | React、Next.js、Vue、Svelte、Node.jsを使用してAI搭載アプリケーションの構築を支援するTypeScriptツールキットです。 |
|
||||
| [API](https://langfuse.com/docs/api) | | 公開APIを直接呼び出すことが可能です。OpenAPI仕様も利用できます。 |
|
||||
| インテグレーション | 対応言語・環境 | 説明 |
|
||||
| ---------------------------------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | SDKを利用して手動でインストゥルメンテーションを実装し、完全な柔軟性を提供します。 |
|
||||
| [OpenAI](https://langfuse.com/integrations/model-providers/openai-py) | Python, JS/TS | OpenAI SDKのドロップイン置換による自動インストゥルメンテーションを実現します。 |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Langchainアプリケーションにコールバックハンドラーを渡すことで自動的に計測します。 |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | LlamaIndexのコールバックシステムを介して自動的にインストゥルメントします。 |
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Haystackのコンテンツトレースシステムを利用した自動インストゥルメンテーションを実現します。 |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | GPTのドロップイン置換として任意のLLMを使用できます。Azure、OpenAI、Cohere、Anthropic、Ollama、VLLM、Sagemaker、HuggingFace、Replicate(100+ LLM)に対応。 |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | React、Next.js、Vue、Svelte、Node.jsを使用してAI搭載アプリケーションの構築を支援するTypeScriptツールキットです。 |
|
||||
| [API](https://langfuse.com/docs/api) | | 公開APIを直接呼び出すことが可能です。OpenAPI仕様も利用できます。 |
|
||||
|
||||
### Langfuseと統合されているパッケージ:
|
||||
|
||||
| 名前 | タイプ | 説明 |
|
||||
| ------------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------- |
|
||||
| [Instructor](https://langfuse.com/docs/integrations/instructor) | ライブラリ | 構造化されたLLM出力(JSON、Pydantic)を取得するためのライブラリ |
|
||||
| [DSPy](https://langfuse.com/docs/integrations/dspy) | ライブラリ | LLMプロンプトや重み付けを体系的に最適化するためのフレームワーク |
|
||||
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | ライブラリ | LLMアプリケーション構築用のPythonツールキット |
|
||||
| [Ollama](https://langfuse.com/docs/integrations/ollama) | モデル(ローカル) | オープンソースLLMを手軽にローカルで実行するためのツール |
|
||||
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | モデル | AWS上でファウンデーションモデルやファインチューニング済みモデルを実行 |
|
||||
| [Google VertexAI and Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | モデル | Google上でファウンデーションモデルやファインチューニング済みモデルを実行 |
|
||||
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | エージェントフレームワーク | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | チャット/エージェント UI | JS/TSのノーコードビルダーで、カスタマイズ可能なLLMフローを構築 |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | チャット/エージェント UI | PythonベースのUIで、react-flowを用いてLangChainの実験やプロトタイピングを容易に実現 |
|
||||
| [Dify](https://langfuse.com/docs/integrations/dify) | チャット/エージェント UI | ノーコードでLLMアプリ開発が可能なオープンソースプラットフォーム |
|
||||
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | チャット/エージェント UI | 自前ホストおよびローカルモデルに対応するLLMチャットWeb UI |
|
||||
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | ツール | オープンソースのLLMテストプラットフォーム |
|
||||
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | チャット/エージェント UI | オープンソースのチャットボットプラットフォーム |
|
||||
| [Vapi](https://langfuse.com/docs/integrations/vapi) | プラットフォーム | オープンソースの音声AIプラットフォーム |
|
||||
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | エージェント | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
|
||||
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | チャット/エージェント UI | チャットUIなどのWebインターフェース構築のためのオープンソースPythonライブラリ |
|
||||
| [Goose](https://langfuse.com/docs/integrations/goose) | エージェント | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
|
||||
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | エージェント | オープンソースのAIエージェントフレームワーク |
|
||||
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | エージェント | エージェントの協調とツール利用を実現するマルチエージェントフレームワーク |
|
||||
| 名前 | タイプ | 説明 |
|
||||
| ------------------------------------------------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| [Instructor](https://langfuse.com/docs/integrations/instructor) | ライブラリ | 構造化されたLLM出力(JSON、Pydantic)を取得するためのライブラリ |
|
||||
| [DSPy](https://langfuse.com/docs/integrations/dspy) | ライブラリ | LLMプロンプトや重み付けを体系的に最適化するためのフレームワーク |
|
||||
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | ライブラリ | LLMアプリケーション構築用のPythonツールキット |
|
||||
| [Ollama](https://langfuse.com/docs/integrations/ollama) | モデル(ローカル) | オープンソースLLMを手軽にローカルで実行するためのツール |
|
||||
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | モデル | AWS上でファウンデーションモデルやファインチューニング済みモデルを実行 |
|
||||
| [Google VertexAI and Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | モデル | Google上でファウンデーションモデルやファインチューニング済みモデルを実行 |
|
||||
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | エージェントフレームワーク | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | チャット/エージェント UI | JS/TSのノーコードビルダーで、カスタマイズ可能なLLMフローを構築 |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | チャット/エージェント UI | PythonベースのUIで、react-flowを用いてLangChainの実験やプロトタイピングを容易に実現 |
|
||||
| [Dify](https://langfuse.com/docs/integrations/dify) | チャット/エージェント UI | ノーコードでLLMアプリ開発が可能なオープンソースプラットフォーム |
|
||||
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | チャット/エージェント UI | 自前ホストおよびローカルモデルに対応するLLMチャットWeb UI |
|
||||
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | ツール | オープンソースのLLMテストプラットフォーム |
|
||||
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | チャット/エージェント UI | オープンソースのチャットボットプラットフォーム |
|
||||
| [Vapi](https://langfuse.com/docs/integrations/vapi) | プラットフォーム | オープンソースの音声AIプラットフォーム |
|
||||
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | エージェント | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
|
||||
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | チャット/エージェント UI | チャットUIなどのWebインターフェース構築のためのオープンソースPythonライブラリ |
|
||||
| [Goose](https://langfuse.com/docs/integrations/goose) | エージェント | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
|
||||
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | エージェント | オープンソースのAIエージェントフレームワーク |
|
||||
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | エージェント | エージェントの協調とツール利用を実現するマルチエージェントフレームワーク |
|
||||
|
||||
## 🚀 クイックスタート
|
||||
|
||||
@@ -200,7 +200,7 @@ Langfuseチームによるマネージドデプロイメント。充実した無
|
||||
### 2️⃣ 初めてのLLM呼び出しのログ記録
|
||||
|
||||
[`@observe()` デコレーター](https://langfuse.com/docs/sdk/python/decorators)を利用することで、任意のPython製LLMアプリケーションのトレースが簡単に行えます。
|
||||
このクイックスタートでは、Langfuseの[OpenAI統合](https://langfuse.com/docs/integrations/openai)を使用して、全てのモデルパラメータを自動で取得します。
|
||||
このクイックスタートでは、Langfuseの[OpenAI統合](https://langfuse.com/integrations/model-providers/openai-py)を使用して、全てのモデルパラメータを自動で取得します。
|
||||
|
||||
> [!TIP]
|
||||
> OpenAIを利用していない場合は、[こちらのドキュメント](https://langfuse.com/docs/get-started#log-your-first-llm-call-to-langfuse)で、他のモデルやフレームワークのログ記録方法をご確認ください。
|
||||
@@ -295,51 +295,51 @@ _[Langfuseの公開トレース例](https://cloud.langfuse.com/project/cloramnkj
|
||||
Langfuseを利用している主要なオープンソースPythonプロジェクト(スター数順): ([出典](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents))
|
||||
|
||||
| リポジトリ | スター |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----: |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/119600397?s=40&v=4" width="20" height="20" alt=""> [twentyhq](https://github.com/twentyhq) / [twenty](https://github.com/twentyhq/twenty) | 24195 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22618 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 15151 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179202840?s=40&v=4" width="20" height="20" alt=""> [mediar-ai](https://github.com/mediar-ai) / [screenpipe](https://github.com/mediar-ai/screenpipe) | 11037 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105877416?s=40&v=4" width="20" height="20" alt=""> [formbricks](https://github.com/formbricks) / [formbricks](https://github.com/formbricks/formbricks) | 9386 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 8385 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7374 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5391 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 4976 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 4141 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2526 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2061 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1161 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1100 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1074 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 971 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 823 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 677 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 639 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 514 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 394 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 384 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 370 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 305 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/154247157?s=40&v=4" width="20" height="20" alt=""> [vespperhq](https://github.com/vespperhq) / [vespper](https://github.com/vespperhq/vespper) | 304 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> [block](https://github.com/block) / [goose](https://github.com/block/goose) | 295 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 291 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 221 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 172 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 148 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -----: |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/119600397?s=40&v=4" width="20" height="20" alt=""> [twentyhq](https://github.com/twentyhq) / [twenty](https://github.com/twentyhq/twenty) | 24195 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22618 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 15151 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179202840?s=40&v=4" width="20" height="20" alt=""> [mediar-ai](https://github.com/mediar-ai) / [screenpipe](https://github.com/mediar-ai/screenpipe) | 11037 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105877416?s=40&v=4" width="20" height="20" alt=""> [formbricks](https://github.com/formbricks) / [formbricks](https://github.com/formbricks/formbricks) | 9386 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 8385 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7374 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5391 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 4976 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 4141 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2526 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2061 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1161 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1100 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1074 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 971 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 823 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 677 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 639 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 514 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 394 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 384 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 370 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 305 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/154247157?s=40&v=4" width="20" height="20" alt=""> [vespperhq](https://github.com/vespperhq) / [vespper](https://github.com/vespperhq/vespper) | 304 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> [block](https://github.com/block) / [goose](https://github.com/block/goose) | 295 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 291 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 221 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 172 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 148 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
|
||||
|
||||
## 🔒 セキュリティとプライバシー
|
||||
|
||||
|
||||
+35
-34
@@ -138,40 +138,40 @@ Langfuse 팀이 관리하는 배포 방식으로, 후한 무료 플랜(취미
|
||||
|
||||
### 주요 통합:
|
||||
|
||||
| 통합 | 지원 | 설명 |
|
||||
| --------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | SDK를 사용하여 완전한 유연성을 갖춘 수동 계측(manual instrumentation)을 수행합니다. |
|
||||
| [OpenAI](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | OpenAI SDK의 드롭인 대체(drop-in replacement)를 통해 자동 계측(automated instrumentation)을 수행합니다. |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Langchain 애플리케이션에 callback 핸들러를 전달하여 자동 계측합니다. |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | LlamaIndex 콜백 시스템을 통한 자동 계측을 지원합니다. |
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Haystack 콘텐츠 추적 시스템을 통한 자동 계측을 지원합니다. |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | GPT의 드롭인 대체품으로 어떤 LLM도 사용할 수 있습니다. Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate 등 100개 이상의 LLM 지원. |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | React, Next.js, Vue, Svelte, Node.js와 함께 AI 기반 애플리케이션 구축을 돕는 TypeScript 툴킷입니다. |
|
||||
| [API](https://langfuse.com/docs/api) | | 공개 API를 직접 호출합니다. OpenAPI 명세가 제공됩니다. |
|
||||
| 통합 | 지원 | 설명 |
|
||||
| ---------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | SDK를 사용하여 완전한 유연성을 갖춘 수동 계측(manual instrumentation)을 수행합니다. |
|
||||
| [OpenAI](https://langfuse.com/integrations/model-providers/openai-py) | Python, JS/TS | OpenAI SDK의 드롭인 대체(drop-in replacement)를 통해 자동 계측(automated instrumentation)을 수행합니다. |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Langchain 애플리케이션에 callback 핸들러를 전달하여 자동 계측합니다. |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | LlamaIndex 콜백 시스템을 통한 자동 계측을 지원합니다. |
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Haystack 콘텐츠 추적 시스템을 통한 자동 계측을 지원합니다. |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | GPT의 드롭인 대체품으로 어떤 LLM도 사용할 수 있습니다. Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate 등 100개 이상의 LLM 지원. |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | React, Next.js, Vue, Svelte, Node.js와 함께 AI 기반 애플리케이션 구축을 돕는 TypeScript 툴킷입니다. |
|
||||
| [API](https://langfuse.com/docs/api) | | 공개 API를 직접 호출합니다. OpenAPI 명세가 제공됩니다. |
|
||||
|
||||
### Langfuse와 통합된 패키지:
|
||||
|
||||
| 이름 | 유형 | 설명 |
|
||||
| ----------------------------------------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| [Instructor](https://langfuse.com/docs/integrations/instructor) | 라이브러리 | 구조화된 LLM 출력을(JSON, Pydantic) 얻기 위한 라이브러리입니다. |
|
||||
| [DSPy](https://langfuse.com/docs/integrations/dspy) | 라이브러리 | 언어 모델 프롬프트와 가중치를 체계적으로 최적화하는 프레임워크입니다. |
|
||||
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | 라이브러리 | LLM 애플리케이션 구축을 위한 Python 툴킷입니다. |
|
||||
| [Ollama](https://langfuse.com/docs/integrations/ollama) | 모델 (로컬) | 자신의 컴퓨터에서 오픈 소스 LLM을 손쉽게 실행할 수 있습니다. |
|
||||
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | 모델 | AWS에서 기본 및 파인튜닝된 모델을 실행합니다. |
|
||||
| [Google VertexAI and Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | 모델 | Google에서 기본 및 파인튜닝된 모델을 실행합니다. |
|
||||
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | 에이전트 프레임워크 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | 채팅/에이전트 UI | 맞춤형 LLM 플로우를 위한 JS/TS 코드 없는(no-code) 빌더입니다. |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | 채팅/에이전트 UI | react-flow를 활용하여 실험 및 프로토타이핑을 손쉽게 할 수 있도록 디자인된 LangChain용 Python 기반 UI입니다. |
|
||||
| [Dify](https://langfuse.com/docs/integrations/dify) | 채팅/에이전트 UI | 코드 없는 빌더와 함께 제공되는 오픈 소스 LLM 애플리케이션 개발 플랫폼입니다. |
|
||||
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | 채팅/에이전트 UI | 셀프 호스팅 및 로컬 모델 등 다양한 LLM 실행기를 지원하는 셀프 호스팅 LLM 채팅 웹 UI입니다. |
|
||||
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | 도구 | 오픈 소스 LLM 테스트 플랫폼입니다. |
|
||||
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | 채팅/에이전트 UI | 오픈 소스 챗봇 플랫폼입니다. |
|
||||
| [Vapi](https://langfuse.com/docs/integrations/vapi) | 플랫폼 | 오픈 소스 음성 AI 플랫폼입니다. |
|
||||
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | 에이전트 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
|
||||
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | 채팅/에이전트 UI | 채팅 UI와 같은 웹 인터페이스 구축을 위한 오픈 소스 Python 라이브러리입니다. |
|
||||
| [Goose](https://langfuse.com/docs/integrations/goose) | 에이전트 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
|
||||
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | 에이전트 | 오픈 소스 AI 에이전트 프레임워크입니다. |
|
||||
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | 에이전트 | 에이전트 간 협업 및 도구 사용을 위한 다중 에이전트 프레임워크입니다. |
|
||||
| 이름 | 유형 | 설명 |
|
||||
| ------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| [Instructor](https://langfuse.com/docs/integrations/instructor) | 라이브러리 | 구조화된 LLM 출력을(JSON, Pydantic) 얻기 위한 라이브러리입니다. |
|
||||
| [DSPy](https://langfuse.com/docs/integrations/dspy) | 라이브러리 | 언어 모델 프롬프트와 가중치를 체계적으로 최적화하는 프레임워크입니다. |
|
||||
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | 라이브러리 | LLM 애플리케이션 구축을 위한 Python 툴킷입니다. |
|
||||
| [Ollama](https://langfuse.com/docs/integrations/ollama) | 모델 (로컬) | 자신의 컴퓨터에서 오픈 소스 LLM을 손쉽게 실행할 수 있습니다. |
|
||||
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | 모델 | AWS에서 기본 및 파인튜닝된 모델을 실행합니다. |
|
||||
| [Google VertexAI and Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | 모델 | Google에서 기본 및 파인튜닝된 모델을 실행합니다. |
|
||||
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | 에이전트 프레임워크 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | 채팅/에이전트 UI | 맞춤형 LLM 플로우를 위한 JS/TS 코드 없는(no-code) 빌더입니다. |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | 채팅/에이전트 UI | react-flow를 활용하여 실험 및 프로토타이핑을 손쉽게 할 수 있도록 디자인된 LangChain용 Python 기반 UI입니다. |
|
||||
| [Dify](https://langfuse.com/docs/integrations/dify) | 채팅/에이전트 UI | 코드 없는 빌더와 함께 제공되는 오픈 소스 LLM 애플리케이션 개발 플랫폼입니다. |
|
||||
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | 채팅/에이전트 UI | 셀프 호스팅 및 로컬 모델 등 다양한 LLM 실행기를 지원하는 셀프 호스팅 LLM 채팅 웹 UI입니다. |
|
||||
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | 도구 | 오픈 소스 LLM 테스트 플랫폼입니다. |
|
||||
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | 채팅/에이전트 UI | 오픈 소스 챗봇 플랫폼입니다. |
|
||||
| [Vapi](https://langfuse.com/docs/integrations/vapi) | 플랫폼 | 오픈 소스 음성 AI 플랫폼입니다. |
|
||||
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | 에이전트 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
|
||||
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | 채팅/에이전트 UI | 채팅 UI와 같은 웹 인터페이스 구축을 위한 오픈 소스 Python 라이브러리입니다. |
|
||||
| [Goose](https://langfuse.com/docs/integrations/goose) | 에이전트 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
|
||||
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | 에이전트 | 오픈 소스 AI 에이전트 프레임워크입니다. |
|
||||
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | 에이전트 | 에이전트 간 협업 및 도구 사용을 위한 다중 에이전트 프레임워크입니다. |
|
||||
|
||||
## 🚀 빠른 시작
|
||||
|
||||
@@ -185,7 +185,7 @@ Langfuse 팀이 관리하는 배포 방식으로, 후한 무료 플랜(취미
|
||||
|
||||
### 2️⃣ 첫 번째 LLM 호출 기록하기
|
||||
|
||||
[`@observe()` 데코레이터](https://langfuse.com/docs/sdk/python/decorators)를 사용하면 Python LLM 애플리케이션의 추적이 매우 간편해집니다. 이 빠른 시작 예제에서는 Langfuse [OpenAI 통합](https://langfuse.com/docs/integrations/openai)을 사용하여 모든 모델 파라미터를 자동으로 캡처합니다.
|
||||
[`@observe()` 데코레이터](https://langfuse.com/docs/sdk/python/decorators)를 사용하면 Python LLM 애플리케이션의 추적이 매우 간편해집니다. 이 빠른 시작 예제에서는 Langfuse [OpenAI 통합](https://langfuse.com/integrations/model-providers/openai-py)을 사용하여 모든 모델 파라미터를 자동으로 캡처합니다.
|
||||
|
||||
> [!TIP]
|
||||
> OpenAI를 사용하지 않으시다면, 다른 모델 및 프레임워크의 로그 기록 방법은 [문서](https://langfuse.com/docs/get-started#log-your-first-llm-call-to-langfuse)를 참조하세요.
|
||||
@@ -276,8 +276,8 @@ _[Langfuse의 공개 예제 trace](https://cloud.langfuse.com/project/cloramnkj0
|
||||
|
||||
별(star) 수를 기준으로 순위가 매겨진 Langfuse를 사용하는 상위 오픈 소스 Python 프로젝트들 ([출처](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents)):
|
||||
|
||||
| 저장소 | 별 |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---: |
|
||||
| 저장소 | 별 |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----: |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
|
||||
@@ -332,6 +332,7 @@ _[Langfuse의 공개 예제 trace](https://cloud.langfuse.com/project/cloramnkj0
|
||||
기본적으로 Langfuse는 자체 호스팅 인스턴스의 기본 사용 통계를 중앙 서버(PostHog)로 자동 보고합니다.
|
||||
|
||||
이를 통해:
|
||||
|
||||
1. Langfuse가 어떻게 사용되는지 이해하고, 가장 관련성 높은 기능을 개선할 수 있습니다.
|
||||
2. 내부 및 외부(예: 자금 조달) 보고를 위한 전체 사용량을 추적할 수 있습니다.
|
||||
|
||||
|
||||
@@ -80,11 +80,11 @@ Langfuse is an **open source LLM engineering** platform. It helps teams collabor
|
||||
|
||||
- [LLM Application Observability](https://langfuse.com/docs/tracing): Instrument your app and start ingesting traces to Langfuse, thereby tracking LLM calls and other relevant logic in your app such as retrieval, embedding, or agent actions. Inspect and debug complex logs and user sessions. Try the interactive [demo](https://langfuse.com/docs/demo) to see this in action.
|
||||
|
||||
- [Prompt Management](https://langfuse.com/docs/prompts/get-started) helps you centrally manage, version control, and collaboratively iterate on your prompts. Thanks to strong caching on server and client side, you can iterate on prompts without adding latency to your application.
|
||||
- [Prompt Management](https://langfuse.com/docs/prompt-management/get-started) helps you centrally manage, version control, and collaboratively iterate on your prompts. Thanks to strong caching on server and client side, you can iterate on prompts without adding latency to your application.
|
||||
|
||||
- [Evaluations](https://langfuse.com/docs/scores/overview) are key to the LLM application development workflow, and Langfuse adapts to your needs. It supports LLM-as-a-judge, user feedback collection, manual labeling, and custom evaluation pipelines via APIs/SDKs.
|
||||
- [Evaluations](https://langfuse.com/docs/evaluation/overview) are key to the LLM application development workflow, and Langfuse adapts to your needs. It supports LLM-as-a-judge, user feedback collection, manual labeling, and custom evaluation pipelines via APIs/SDKs.
|
||||
|
||||
- [Datasets](https://langfuse.com/docs/datasets/overview) enable test sets and benchmarks for evaluating your LLM application. They support continuous improvement, pre-deployment testing, structured experiments, flexible evaluation, and seamless integration with frameworks like LangChain and LlamaIndex.
|
||||
- [Datasets](https://langfuse.com/docs/evaluation/dataset-runs/datasets) enable test sets and benchmarks for evaluating your LLM application. They support continuous improvement, pre-deployment testing, structured experiments, flexible evaluation, and seamless integration with frameworks like LangChain and LlamaIndex.
|
||||
|
||||
- [LLM Playground](https://langfuse.com/docs/playground) is a tool for testing and iterating on your prompts and model configurations, shortening the feedback loop and accelerating development. When you see a bad result in tracing, you can directly jump to the playground to iterate on it.
|
||||
|
||||
@@ -118,6 +118,7 @@ Run Langfuse on your own infrastructure:
|
||||
# Run the langfuse docker compose
|
||||
docker compose up
|
||||
```
|
||||
|
||||
- [VM](https://langfuse.com/self-hosting/docker-compose): Run Langfuse on a single Virtual Machine using Docker Compose.
|
||||
- [Kubernetes (Helm)](https://langfuse.com/self-hosting/kubernetes-helm): Run Langfuse on a Kubernetes cluster using Helm. This is the preferred production deployment.
|
||||
- Terraform Templates: [AWS](https://langfuse.com/self-hosting/aws), [Azure](https://langfuse.com/self-hosting/azure), [GCP](https://langfuse.com/self-hosting/gcp)
|
||||
@@ -133,7 +134,7 @@ See [self-hosting documentation](https://langfuse.com/self-hosting) to learn mor
|
||||
| Integration | Supports | Description |
|
||||
| ---------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
|
||||
| [OpenAI](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
|
||||
| [OpenAI](https://langfuse.com/integrations/model-providers/openai-py) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | Automated instrumentation via LlamaIndex callback system. |
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
|
||||
@@ -176,7 +177,7 @@ Instrument your app and start ingesting traces to Langfuse, thereby tracking LLM
|
||||
|
||||
### 2️⃣ Log your first LLM call
|
||||
|
||||
The [`@observe()` decorator](https://langfuse.com/docs/sdk/python/decorators) makes it easy to trace any Python LLM application. In this quickstart we also use the Langfuse [OpenAI integration](https://langfuse.com/docs/integrations/openai) to automatically capture all model parameters.
|
||||
The [`@observe()` decorator](https://langfuse.com/docs/sdk/python/decorators) makes it easy to trace any Python LLM application. In this quickstart we also use the Langfuse [OpenAI integration](https://langfuse.com/integrations/model-providers/openai-py) to automatically capture all model parameters.
|
||||
|
||||
> [!TIP]
|
||||
> Not using OpenAI? Visit [our documentation](https://langfuse.com/docs/get-started#log-your-first-llm-call-to-langfuse) to learn how to log other models and frameworks.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# 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.
|
||||
- When adding new indexes on ClickHouse, ensure that there is a corresponding `MATERIALIZE INDEX` statement in the same migration. The materialization can use `SETTINGS mutations_sync = 2` if they operate on smaller tables, but may timeout otherwise.
|
||||
|
||||
### Postgres
|
||||
|
||||
- Most `schema.prisma` changes should produce a change in `packages/shared/prisma/migrations`.
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,7 @@ service:
|
||||
I.e. if you want to update a trace, you'd use the same body id, but separate event IDs.
|
||||
|
||||
Notes:
|
||||
- Introduction to data model: https://langfuse.com/docs/tracing-data-model
|
||||
- Introduction to data model: https://langfuse.com/docs/observability/data-model
|
||||
- Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly.
|
||||
- The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors.
|
||||
method: POST
|
||||
@@ -145,6 +145,13 @@ types:
|
||||
- SPAN
|
||||
- GENERATION
|
||||
- EVENT
|
||||
- AGENT
|
||||
- TOOL
|
||||
- CHAIN
|
||||
- RETRIEVER
|
||||
- EVALUATOR
|
||||
- EMBEDDING
|
||||
- GUARDRAIL
|
||||
|
||||
IngestionUsage:
|
||||
discriminated: false
|
||||
|
||||
@@ -20,6 +20,13 @@ service:
|
||||
request: MembershipRequest
|
||||
response: MembershipResponse
|
||||
|
||||
deleteOrganizationMembership:
|
||||
docs: Delete a membership from the organization associated with the API key (requires organization-scoped API key)
|
||||
method: DELETE
|
||||
path: /organizations/memberships
|
||||
request: DeleteMembershipRequest
|
||||
response: MembershipDeletionResponse
|
||||
|
||||
getProjectMemberships:
|
||||
docs: Get all memberships for a specific project (requires organization-scoped API key)
|
||||
method: GET
|
||||
@@ -37,6 +44,15 @@ service:
|
||||
request: MembershipRequest
|
||||
response: MembershipResponse
|
||||
|
||||
deleteProjectMembership:
|
||||
docs: Delete a membership from a specific project (requires organization-scoped API key). The user must be a member of the organization.
|
||||
method: DELETE
|
||||
path: /projects/{projectId}/memberships
|
||||
path-parameters:
|
||||
projectId: string
|
||||
request: DeleteMembershipRequest
|
||||
response: MembershipDeletionResponse
|
||||
|
||||
getOrganizationProjects:
|
||||
docs: Get all projects for the organization associated with the API key (requires organization-scoped API key)
|
||||
method: GET
|
||||
@@ -56,6 +72,10 @@ types:
|
||||
userId: string
|
||||
role: MembershipRole
|
||||
|
||||
DeleteMembershipRequest:
|
||||
properties:
|
||||
userId: string
|
||||
|
||||
MembershipResponse:
|
||||
properties:
|
||||
userId: string
|
||||
@@ -63,6 +83,11 @@ types:
|
||||
email: string
|
||||
name: string
|
||||
|
||||
MembershipDeletionResponse:
|
||||
properties:
|
||||
message: string
|
||||
userId: string
|
||||
|
||||
MembershipsResponse:
|
||||
properties:
|
||||
memberships: list<MembershipResponse>
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "3.97.2",
|
||||
"version": "3.99.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -40,7 +40,7 @@
|
||||
"husky": "^9.0.11",
|
||||
"prettier": "^3.6.2",
|
||||
"release-it": "^19.0.3",
|
||||
"turbo": "^2.5.5"
|
||||
"turbo": "^2.5.6"
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
|
||||
@@ -41,6 +41,14 @@ module.exports = {
|
||||
// https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||
rules: {
|
||||
"no-undef": "off",
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
name: "redis",
|
||||
message:
|
||||
"Import redis explicitly from '@langfuse/shared/src/server' instead of using global.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"@vercel/style-guide": "^6.0.0",
|
||||
"eslint-config-next": "^14.2.15",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-turbo": "^2.5.5",
|
||||
"eslint-config-turbo": "^2.5.6",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE observations ON CLUSTER default DROP INDEX IF EXISTS idx_res_metadata_value;
|
||||
ALTER TABLE observations ON CLUSTER default DROP INDEX IF EXISTS idx_res_metadata_key;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE observations ON CLUSTER default ADD INDEX IF NOT EXISTS idx_res_metadata_key mapKeys(metadata) TYPE bloom_filter(0.01) GRANULARITY 1;
|
||||
ALTER TABLE observations ON CLUSTER default ADD INDEX IF NOT EXISTS idx_res_metadata_value mapValues(metadata) TYPE bloom_filter(0.01) GRANULARITY 1;
|
||||
ALTER TABLE observations ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_res_metadata_key;
|
||||
ALTER TABLE observations ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_res_metadata_value;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE dataset_run_items_rmt ON CLUSTER default DROP INDEX IF EXISTS idx_trace_id;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE dataset_run_items_rmt ON CLUSTER default ADD INDEX IF NOT EXISTS idx_trace_id trace_id TYPE bloom_filter(0.001) GRANULARITY 1;
|
||||
ALTER TABLE dataset_run_items_rmt ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_trace_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);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE observations DROP INDEX IF EXISTS idx_res_metadata_value;
|
||||
ALTER TABLE observations DROP INDEX IF EXISTS idx_res_metadata_key;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE observations ADD INDEX IF NOT EXISTS idx_res_metadata_key mapKeys(metadata) TYPE bloom_filter(0.01) GRANULARITY 1;
|
||||
ALTER TABLE observations ADD INDEX IF NOT EXISTS idx_res_metadata_value mapValues(metadata) TYPE bloom_filter(0.01) GRANULARITY 1;
|
||||
ALTER TABLE observations MATERIALIZE INDEX IF EXISTS idx_res_metadata_key;
|
||||
ALTER TABLE observations MATERIALIZE INDEX IF EXISTS idx_res_metadata_value;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE dataset_run_items_rmt DROP INDEX IF EXISTS idx_trace_id;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE dataset_run_items_rmt ADD INDEX IF NOT EXISTS idx_trace_id trace_id TYPE bloom_filter(0.001) GRANULARITY 1;
|
||||
ALTER TABLE dataset_run_items_rmt MATERIALIZE INDEX IF EXISTS idx_trace_id;
|
||||
@@ -61,7 +61,7 @@
|
||||
"@aws-sdk/lib-storage": "^3.675.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.679.0",
|
||||
"@azure/storage-blob": "^12.26.0",
|
||||
"@clickhouse/client": "^1.12.0",
|
||||
"@clickhouse/client": "^1.12.1",
|
||||
"@google-cloud/storage": "^7.15.2",
|
||||
"@langchain/anthropic": "^0.3.22",
|
||||
"@langchain/aws": "^0.1.11",
|
||||
|
||||
@@ -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];
|
||||
|
||||
+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', '{}');
|
||||
@@ -368,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
|
||||
@@ -409,6 +408,13 @@ enum LegacyPrismaObservationType {
|
||||
SPAN
|
||||
EVENT
|
||||
GENERATION
|
||||
AGENT
|
||||
TOOL
|
||||
CHAIN
|
||||
RETRIEVER
|
||||
EVALUATOR
|
||||
EMBEDDING
|
||||
GUARDRAIL
|
||||
|
||||
@@map("ObservationType")
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -158,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()
|
||||
@@ -193,6 +199,14 @@ const EnvSchema = z.object({
|
||||
SLACK_CLIENT_ID: z.string().optional(),
|
||||
SLACK_CLIENT_SECRET: z.string().optional(),
|
||||
SLACK_STATE_SECRET: z.string().optional(),
|
||||
SLACK_FETCH_LIMIT: z
|
||||
.number()
|
||||
.positive()
|
||||
.optional()
|
||||
.default(1_000)
|
||||
.describe(
|
||||
"How many records should be fetched from Slack, before we give up",
|
||||
),
|
||||
HTTPS_PROXY: z.string().optional(),
|
||||
|
||||
LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT: z.coerce
|
||||
|
||||
@@ -5,6 +5,13 @@ export const langfuseObjects = [
|
||||
"span",
|
||||
"generation",
|
||||
"event",
|
||||
"agent",
|
||||
"tool",
|
||||
"chain",
|
||||
"retriever",
|
||||
"evaluator",
|
||||
"embedding",
|
||||
"guardrail",
|
||||
"dataset_item",
|
||||
] as const;
|
||||
|
||||
@@ -51,6 +58,56 @@ const observationCols = [
|
||||
];
|
||||
|
||||
export const availableTraceEvalVariables = [
|
||||
{
|
||||
id: "agent",
|
||||
display: "Agent",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "chain",
|
||||
display: "Chain",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "embedding",
|
||||
display: "Embedding",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "evaluator",
|
||||
display: "Evaluator",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "event",
|
||||
display: "Event",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "generation",
|
||||
display: "Generation",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "guardrail",
|
||||
display: "Guardrail",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "retriever",
|
||||
display: "Retriever",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "span",
|
||||
display: "Span",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "tool",
|
||||
display: "Tool",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "trace",
|
||||
display: "Trace",
|
||||
@@ -65,21 +122,6 @@ export const availableTraceEvalVariables = [
|
||||
{ name: "Output", id: "output", internal: 't."output"' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "span",
|
||||
display: "Span",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "generation",
|
||||
display: "Generation",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "event",
|
||||
display: "Event",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
];
|
||||
|
||||
export const availableDatasetEvalVariables = [
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { logger } from "./logger";
|
||||
import { redis } from "./";
|
||||
|
||||
/**
|
||||
* Redis cache utilities for eval job configuration optimization.
|
||||
*
|
||||
* This module provides caching functionality to reduce database calls when
|
||||
* checking for job configurations. When a project has no active evaluation
|
||||
* job configurations, we cache this information in Redis to avoid unnecessary
|
||||
* database queries and queue processing.
|
||||
*/
|
||||
|
||||
const NO_JOB_CONFIG_PREFIX = "langfuse:eval:no-job-configs";
|
||||
|
||||
/**
|
||||
* Check if a project has no job configurations cached in Redis.
|
||||
* Returns true if the cache indicates no eval job configs exist.
|
||||
*
|
||||
* @param projectId - The project ID to check cache for
|
||||
* @returns Promise<boolean> - true if no eval job configs present
|
||||
*/
|
||||
export const hasNoJobConfigsCache = async (
|
||||
projectId: string,
|
||||
): Promise<boolean> => {
|
||||
if (!redis) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const cacheKey = `${NO_JOB_CONFIG_PREFIX}:${projectId}`;
|
||||
const cached = await redis.get(cacheKey);
|
||||
return Boolean(cached);
|
||||
} catch (error) {
|
||||
logger.error("Failed to check no eval job configs cache", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cache that a project has no active job configurations.
|
||||
* This is set when a database query returns no active EVAL job configurations.
|
||||
* The cache expires after 10 minutes to ensure eventual consistency.
|
||||
*
|
||||
* @param projectId - The project ID to cache
|
||||
*/
|
||||
export const setNoJobConfigsCache = async (
|
||||
projectId: string,
|
||||
): Promise<void> => {
|
||||
if (!redis) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const cacheKey = `${NO_JOB_CONFIG_PREFIX}:${projectId}`;
|
||||
await redis.setex(cacheKey, 600, "1"); // Cache for 10 minutes
|
||||
logger.debug(`Cached no eval job configs for project ${projectId}`);
|
||||
} catch (error) {
|
||||
logger.error("Failed to cache no eval job configs status", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the "no eval job configs" cache for a project.
|
||||
* Should be called when job configurations are created or activated.
|
||||
*
|
||||
* @param projectId - The project ID to clear cache for
|
||||
*/
|
||||
export const clearNoJobConfigsCache = async (
|
||||
projectId: string,
|
||||
): Promise<void> => {
|
||||
if (!redis) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const cacheKey = `${NO_JOB_CONFIG_PREFIX}:${projectId}`;
|
||||
await redis.del(cacheKey);
|
||||
logger.debug(`Cleared no eval job configs cache for project ${projectId}`);
|
||||
} catch (error) {
|
||||
logger.error("Failed to clear no eval job configs cache", error);
|
||||
}
|
||||
};
|
||||
@@ -7,6 +7,7 @@ export * from "./services/PromptService";
|
||||
export * from "./services/PromptService/types";
|
||||
export * from "./services/traces-ui-table-service";
|
||||
export * from "./services/InMemoryFilterService";
|
||||
export * from "./evalJobConfigCache";
|
||||
export * from "./auth/apiKeys";
|
||||
export * from "./auth/customSsoProvider";
|
||||
export * from "./auth/gitHubEnterpriseProvider";
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -242,6 +242,7 @@ export async function fetchLLMCompletion(
|
||||
timeout: 1000 * 60 * 2, // 2 minutes timeout
|
||||
...(proxyAgent && { httpAgent: proxyAgent }),
|
||||
},
|
||||
invocationKwargs: modelParams.providerOptions,
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.OpenAI) {
|
||||
chatModel = new ChatOpenAI({
|
||||
@@ -258,6 +259,7 @@ export async function fetchLLMCompletion(
|
||||
defaultHeaders: extraHeaders,
|
||||
...(proxyAgent && { httpAgent: proxyAgent }),
|
||||
},
|
||||
modelKwargs: modelParams.providerOptions,
|
||||
timeout: 1000 * 60 * 2, // 2 minutes timeout
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.Azure) {
|
||||
@@ -276,6 +278,7 @@ export async function fetchLLMCompletion(
|
||||
defaultHeaders: extraHeaders,
|
||||
...(proxyAgent && { httpAgent: proxyAgent }),
|
||||
},
|
||||
modelKwargs: modelParams.providerOptions,
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.Bedrock) {
|
||||
const { region } = BedrockConfigSchema.parse(config);
|
||||
@@ -295,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));
|
||||
@@ -351,52 +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,
|
||||
...(proxyAgent && { httpAgent: proxyAgent }),
|
||||
},
|
||||
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",
|
||||
|
||||
@@ -22,7 +22,7 @@ export const testModelCall = async ({
|
||||
apiKey: z.infer<typeof LLMApiKeySchema>;
|
||||
prompt?: string;
|
||||
modelConfig?: ModelConfig | null;
|
||||
}): Promise<void> => {
|
||||
}) => {
|
||||
(
|
||||
await fetchLLMCompletion({
|
||||
streaming: false,
|
||||
|
||||
@@ -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
|
||||
@@ -285,7 +287,7 @@ export const ExperimentMetadataSchema = z
|
||||
.strict();
|
||||
export type ExperimentMetadata = z.infer<typeof ExperimentMetadataSchema>;
|
||||
|
||||
// NOTE: Update docs page when changing this! https://langfuse.com/docs/playground#openai-playground--anthropic-playground
|
||||
// NOTE: Update docs page when changing this! https://langfuse.com/docs/prompt-management/features/playground#openai-playground--anthropic-playground
|
||||
// WARNING: The first entry in the array is chosen as the default model to add LLM API keys
|
||||
export const openAIModels = [
|
||||
"gpt-4.1",
|
||||
@@ -333,7 +335,7 @@ export const openAIModels = [
|
||||
|
||||
export type OpenAIModel = (typeof openAIModels)[number];
|
||||
|
||||
// NOTE: Update docs page when changing this! https://langfuse.com/docs/playground#openai-playground--anthropic-playground
|
||||
// NOTE: Update docs page when changing this! https://langfuse.com/docs/prompt-management/features/playground#openai-playground--anthropic-playground
|
||||
// WARNING: The first entry in the array is chosen as the default model to add LLM API keys
|
||||
export const anthropicModels = [
|
||||
"claude-sonnet-4-20250514",
|
||||
|
||||
@@ -17,14 +17,30 @@ import { DatasetRunItemRecordReadType } from "./definitions";
|
||||
import { env } from "../../env";
|
||||
import { commandClickhouse } from "./clickhouse";
|
||||
import Decimal from "decimal.js";
|
||||
import { ClickHouseClientConfigOptions } from "@clickhouse/client";
|
||||
|
||||
type DatasetItemIdsByTraceIdQuery = {
|
||||
projectId: string;
|
||||
traceId: string;
|
||||
// this filter should include a dataset_id filter to search along primary key
|
||||
filter: FilterState;
|
||||
};
|
||||
|
||||
type DatasetRunItemsTableQuery = {
|
||||
projectId: string;
|
||||
datasetId: string;
|
||||
filter: FilterState;
|
||||
datasetId?: string;
|
||||
orderBy?: OrderByState | OrderByState[];
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
clickhouseConfigs?: ClickHouseClientConfigOptions;
|
||||
};
|
||||
|
||||
type DatasetRunItemsByDatasetIdQuery = Omit<
|
||||
DatasetRunItemsTableQuery,
|
||||
"datasetId"
|
||||
> & {
|
||||
datasetId: string;
|
||||
};
|
||||
|
||||
type DatasetRunsMetricsTableQuery = {
|
||||
@@ -73,22 +89,26 @@ const convertDatasetRunsMetricsRecord = (
|
||||
|
||||
const getProjectDatasetIdDefaultFilter = (
|
||||
projectId: string,
|
||||
datasetId: string,
|
||||
datasetId?: string,
|
||||
) => {
|
||||
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",
|
||||
field: "dataset_id",
|
||||
operator: "=",
|
||||
value: datasetId,
|
||||
}),
|
||||
...(datasetId
|
||||
? [
|
||||
new StringFilter({
|
||||
clickhouseTable: "dataset_run_items_rmt",
|
||||
field: "dataset_id",
|
||||
operator: "=",
|
||||
value: datasetId,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
]),
|
||||
};
|
||||
};
|
||||
@@ -132,13 +152,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 +170,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 +183,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 +210,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 +337,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" : ""}
|
||||
@@ -333,14 +353,15 @@ const getDatasetRunItemsTableInternal = async <T>(
|
||||
feature: "datasets",
|
||||
type: "dataset-run-items",
|
||||
projectId,
|
||||
datasetId,
|
||||
...(datasetId ? { datasetId } : {}),
|
||||
},
|
||||
clickhouseConfigs: opts.clickhouseConfigs,
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getDatasetRunItemsByDatasetIdCh = async (
|
||||
export const getDatasetRunItemsCh = async (
|
||||
opts: DatasetRunItemsTableQuery,
|
||||
): Promise<DatasetRunItemDomain[]> => {
|
||||
const rows =
|
||||
@@ -353,7 +374,80 @@ export const getDatasetRunItemsByDatasetIdCh = async (
|
||||
return rows.map(convertDatasetRunItemClickhouseToDomain);
|
||||
};
|
||||
|
||||
export const getDatasetRunItemsCountByDatasetIdCh = async (
|
||||
export const getDatasetRunItemsByDatasetIdCh = async (
|
||||
opts: DatasetRunItemsByDatasetIdQuery,
|
||||
): Promise<DatasetRunItemDomain[]> => {
|
||||
const rows =
|
||||
await getDatasetRunItemsTableInternal<DatasetRunItemRecordReadType>({
|
||||
...opts,
|
||||
select: "rows",
|
||||
tags: { kind: "list" },
|
||||
});
|
||||
|
||||
return rows.map(convertDatasetRunItemClickhouseToDomain);
|
||||
};
|
||||
|
||||
export const getDatasetItemIdsByTraceIdCh = async (
|
||||
opts: DatasetItemIdsByTraceIdQuery,
|
||||
): Promise<{ id: string; datasetId: string }[]> => {
|
||||
const { projectId, traceId, filter } = opts;
|
||||
|
||||
const datasetRunItemsFilter = new FilterList([
|
||||
new StringFilter({
|
||||
clickhouseTable: "dataset_run_items_rmt",
|
||||
field: "project_id",
|
||||
operator: "=",
|
||||
value: projectId,
|
||||
}),
|
||||
new StringFilter({
|
||||
clickhouseTable: "dataset_run_items_rmt",
|
||||
field: "trace_id",
|
||||
operator: "=",
|
||||
value: traceId,
|
||||
}),
|
||||
]);
|
||||
|
||||
datasetRunItemsFilter.push(
|
||||
...createFilterFromFilterState(
|
||||
filter,
|
||||
datasetRunItemsTableUiColumnDefinitions,
|
||||
),
|
||||
);
|
||||
const appliedFilter = datasetRunItemsFilter.apply();
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
dri.dataset_item_id as dataset_item_id,
|
||||
dri.dataset_id as dataset_id
|
||||
FROM dataset_run_items_rmt dri
|
||||
WHERE ${appliedFilter.query}
|
||||
LIMIT 1 BY dri.project_id, dri.dataset_id, dri.dataset_run_id, dri.dataset_item_id;`;
|
||||
|
||||
const res = await queryClickhouse<{
|
||||
dataset_item_id: string;
|
||||
dataset_id: string;
|
||||
}>({
|
||||
query,
|
||||
params: {
|
||||
...appliedFilter.params,
|
||||
},
|
||||
tags: {
|
||||
feature: "datasets",
|
||||
type: "dataset-run-items",
|
||||
projectId,
|
||||
traceId,
|
||||
},
|
||||
});
|
||||
|
||||
return res.map((runItem) => {
|
||||
return {
|
||||
id: runItem.dataset_item_id,
|
||||
datasetId: runItem.dataset_id,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const getDatasetRunItemsCountCh = async (
|
||||
opts: DatasetRunItemsTableQuery,
|
||||
): Promise<number> => {
|
||||
const rows = await getDatasetRunItemsTableInternal<{ count: string }>({
|
||||
@@ -365,13 +459,25 @@ export const getDatasetRunItemsCountByDatasetIdCh = async (
|
||||
return Number(rows[0]?.count);
|
||||
};
|
||||
|
||||
export const getDatasetRunItemsCountByDatasetIdCh = async (
|
||||
opts: DatasetRunItemsByDatasetIdQuery,
|
||||
): Promise<number> => {
|
||||
const rows = await getDatasetRunItemsTableInternal<{ count: string }>({
|
||||
...opts,
|
||||
select: "count",
|
||||
tags: { kind: "list" },
|
||||
});
|
||||
|
||||
return Number(rows[0]?.count);
|
||||
};
|
||||
|
||||
export const deleteDatasetRunItemsByProjectId = async ({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) => {
|
||||
const query = `
|
||||
DELETE FROM dataset_run_items
|
||||
DELETE FROM dataset_run_items_rmt
|
||||
WHERE project_id = {projectId: String};
|
||||
`;
|
||||
await commandClickhouse({
|
||||
@@ -399,7 +505,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 +538,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)})
|
||||
|
||||
@@ -297,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}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -725,27 +738,28 @@ export const getTraceById = async ({
|
||||
const query = `
|
||||
SELECT
|
||||
id,
|
||||
name as name,
|
||||
user_id as user_id,
|
||||
metadata as metadata,
|
||||
release as release,
|
||||
version as version,
|
||||
anyLast(name) as name,
|
||||
anyLast(user_id) as user_id,
|
||||
maxMap(metadata) as metadata,
|
||||
anyLast(release) as release,
|
||||
anyLast(version) as version,
|
||||
project_id,
|
||||
environment,
|
||||
finalizeAggregation(public) as public,
|
||||
finalizeAggregation(bookmarked) as bookmarked,
|
||||
tags,
|
||||
${renderingProps.truncated ? `left(finalizeAggregation(input), ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "finalizeAggregation(input)"} as input,
|
||||
${renderingProps.truncated ? `left(finalizeAggregation(output), ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "finalizeAggregation(output)"} as output,
|
||||
session_id as session_id,
|
||||
anyLast(environment) as environment,
|
||||
argMaxMerge(public) as public,
|
||||
argMaxMerge(bookmarked) as bookmarked,
|
||||
groupUniqArrayArray(tags) as tags,
|
||||
${renderingProps.truncated ? `left(argMaxMerge(input), ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "argMaxMerge(input)"} as input,
|
||||
${renderingProps.truncated ? `left(argMaxMerge(output), ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "argMaxMerge(output)"} as output,
|
||||
anyLast(session_id) as session_id,
|
||||
0 as is_deleted,
|
||||
start_time as timestamp,
|
||||
created_at,
|
||||
updated_at,
|
||||
updated_at as event_ts
|
||||
FROM traces_all_amt
|
||||
min(start_time) as timestamp,
|
||||
min(start_time) as created_at,
|
||||
max(t.updated_at) as updated_at,
|
||||
max(t.updated_at) as event_ts
|
||||
FROM traces_all_amt t
|
||||
WHERE id = {traceId: String}
|
||||
AND project_id = {projectId: String}
|
||||
GROUP BY project_id, id
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ export function parseSlackInstallationMetadata(
|
||||
*/
|
||||
export class SlackService {
|
||||
private static instance: SlackService | null = null;
|
||||
private installer: InstallProvider;
|
||||
private readonly installer: InstallProvider;
|
||||
|
||||
private constructor() {
|
||||
this.installer = new InstallProvider({
|
||||
@@ -296,14 +296,20 @@ export class SlackService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get channels accessible to the bot
|
||||
* Recursively fetch all channels accessible to the bot
|
||||
* Uses cursor-based pagination defined by Slack API https://api.slack.com/apis/pagination
|
||||
*/
|
||||
async getChannels(client: WebClient): Promise<SlackChannel[]> {
|
||||
private async getChannelsRecursive(
|
||||
client: WebClient,
|
||||
cursor?: string,
|
||||
fetchedRecords: number = 0,
|
||||
): Promise<SlackChannel[]> {
|
||||
try {
|
||||
const result = await client.conversations.list({
|
||||
exclude_archived: true,
|
||||
types: "public_channel",
|
||||
limit: 200,
|
||||
cursor: cursor,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
@@ -319,6 +325,41 @@ export class SlackService {
|
||||
}),
|
||||
);
|
||||
|
||||
const nextCursor = result.response_metadata?.next_cursor;
|
||||
if (
|
||||
nextCursor &&
|
||||
fetchedRecords + channels.length < env.SLACK_FETCH_LIMIT
|
||||
) {
|
||||
try {
|
||||
const nextPageChannels = await this.getChannelsRecursive(
|
||||
client,
|
||||
nextCursor,
|
||||
fetchedRecords + channels.length,
|
||||
);
|
||||
return [...channels, ...nextPageChannels];
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to retrieve next page of channels, returning only already fetched`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
return channels;
|
||||
} catch (error) {
|
||||
logger.error("Failed to fetch channels recursively", { error, cursor });
|
||||
throw new Error(
|
||||
`Failed to fetch channels: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get channels accessible to the bot
|
||||
*/
|
||||
async getChannels(client: WebClient): Promise<SlackChannel[]> {
|
||||
try {
|
||||
const channels = await this.getChannelsRecursive(client);
|
||||
|
||||
logger.debug("Retrieved channels from Slack", {
|
||||
channelCount: channels.length,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,28 +1,60 @@
|
||||
import { UiColumnMappings } from "./types";
|
||||
import { DatasetRunItemDomain } from "../domain/dataset-run-items";
|
||||
|
||||
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"',
|
||||
},
|
||||
{
|
||||
uiTableName: "Dataset",
|
||||
uiTableId: "datasetId",
|
||||
clickhouseTableName: "dataset_run_items_rmt",
|
||||
clickhouseSelect: 'dri."dataset_id"',
|
||||
},
|
||||
];
|
||||
|
||||
export const mapDatasetRunItemFilterColumn = (
|
||||
dataset: Pick<DatasetRunItemDomain, "id" | "datasetId">,
|
||||
column: string,
|
||||
): unknown => {
|
||||
const columnDef = datasetRunItemsTableUiColumnDefinitions.find(
|
||||
(col) =>
|
||||
col.uiTableId === column ||
|
||||
col.uiTableName === column ||
|
||||
col.clickhouseSelect === column,
|
||||
);
|
||||
if (!columnDef) {
|
||||
throw new Error(`Unhandled column for dataset run items filter: ${column}`);
|
||||
}
|
||||
switch (columnDef.uiTableId) {
|
||||
case "id":
|
||||
return dataset.id;
|
||||
case "datasetId":
|
||||
return dataset.datasetId;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled column in dataset run items filter mapping: ${column}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
+51
-57
@@ -39,8 +39,8 @@ importers:
|
||||
specifier: ^19.0.3
|
||||
version: 19.0.3(@types/node@20.14.8)
|
||||
turbo:
|
||||
specifier: ^2.5.5
|
||||
version: 2.5.5
|
||||
specifier: ^2.5.6
|
||||
version: 2.5.6
|
||||
|
||||
ee:
|
||||
dependencies:
|
||||
@@ -118,8 +118,8 @@ importers:
|
||||
specifier: ^9.1.0
|
||||
version: 9.1.0(eslint@8.57.0)
|
||||
eslint-config-turbo:
|
||||
specifier: ^2.5.5
|
||||
version: 2.5.5(eslint@8.57.0)(turbo@2.5.5)
|
||||
specifier: ^2.5.6
|
||||
version: 2.5.6(eslint@8.57.0)(turbo@2.5.6)
|
||||
eslint-plugin-only-warn:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
@@ -150,8 +150,8 @@ importers:
|
||||
specifier: ^12.26.0
|
||||
version: 12.26.0
|
||||
'@clickhouse/client':
|
||||
specifier: ^1.12.0
|
||||
version: 1.12.0
|
||||
specifier: ^1.12.1
|
||||
version: 1.12.1
|
||||
'@google-cloud/storage':
|
||||
specifier: ^7.15.2
|
||||
version: 7.15.2
|
||||
@@ -1785,11 +1785,11 @@ packages:
|
||||
'@chevrotain/utils@11.0.3':
|
||||
resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==}
|
||||
|
||||
'@clickhouse/client-common@1.12.0':
|
||||
resolution: {integrity: sha512-cyI4n7u9jK30d9q1q0ceQ7IwJ/MtTs5HxoQfc8yHpN+ok5wqaU2jAtq5hpa1z7C7sS1pDy/ZOFmOzg1v1F683g==}
|
||||
'@clickhouse/client-common@1.12.1':
|
||||
resolution: {integrity: sha512-ccw1N6hB4+MyaAHIaWBwGZ6O2GgMlO99FlMj0B0UEGfjxM9v5dYVYql6FpP19rMwrVAroYs/IgX2vyZEBvzQLg==}
|
||||
|
||||
'@clickhouse/client@1.12.0':
|
||||
resolution: {integrity: sha512-vJUSX8THhTzlVn0WxPukVjOgNRaSoY02ubQkB0LpqNoHFxXuF5jQZZAYvGZWpBGbYQ/4gfPrqu8g4TX5UKeNxA==}
|
||||
'@clickhouse/client@1.12.1':
|
||||
resolution: {integrity: sha512-7ORY85rphRazqHzImNXMrh4vsaPrpetFoTWpZYueCO2bbO6PXYDXp/GQ4DgxnGIqbWB/Di1Ai+Xuwq2o7DJ36A==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
'@codemirror/autocomplete@6.17.0':
|
||||
@@ -7199,10 +7199,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dotenv@16.5.0:
|
||||
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dotenv@16.6.1:
|
||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -7393,8 +7389,8 @@ packages:
|
||||
eslint-plugin-n: '^15.0.0 || ^16.0.0 '
|
||||
eslint-plugin-promise: ^6.0.0
|
||||
|
||||
eslint-config-turbo@2.5.5:
|
||||
resolution: {integrity: sha512-23lKrCr66HQR62aa94n2dBGUUFz0GzM6N1KwmcREZHxomZ5Ik2rDm00wAz95lOEkhzSt6IaxW00Uz0az/Fcq5Q==}
|
||||
eslint-config-turbo@2.5.6:
|
||||
resolution: {integrity: sha512-1EV/UqdKE75st9q6y0MCxz7qp2v7RyGvbQsMLSuCz+VH8ScnSfmhd8FcAbqx3BshCy5IluujzMB6T5iCgL3/sA==}
|
||||
peerDependencies:
|
||||
eslint: '>6.6.0'
|
||||
turbo: '>2.0.0'
|
||||
@@ -7538,8 +7534,8 @@ packages:
|
||||
eslint-plugin-tsdoc@0.2.17:
|
||||
resolution: {integrity: sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA==}
|
||||
|
||||
eslint-plugin-turbo@2.5.5:
|
||||
resolution: {integrity: sha512-IlN65X6W7rgK88u5xl1xC+7FIGKA7eyaca0yxZQ9CBNV6keAaqtjZQLw8ZfXdv7T+MzTLYkYOeOHAv8yCRUx4Q==}
|
||||
eslint-plugin-turbo@2.5.6:
|
||||
resolution: {integrity: sha512-KUDE23aP2JV8zbfZ4TeM1HpAXzMM/AYG/bJam7P4AalUxas8Pd/lS/6R3p4uX91qJcH1LwL4h0ED48nDe8KorQ==}
|
||||
peerDependencies:
|
||||
eslint: '>6.6.0'
|
||||
turbo: '>2.0.0'
|
||||
@@ -11587,38 +11583,38 @@ packages:
|
||||
ttl-set@1.0.0:
|
||||
resolution: {integrity: sha512-2fuHn/UR+8Z9HK49r97+p2Ru1b5Eewg2QqPrU14BVCQ9QoyU3+vLLZk2WEiyZ9sgJh6W8G1cZr9I2NBLywAHrA==}
|
||||
|
||||
turbo-darwin-64@2.5.5:
|
||||
resolution: {integrity: sha512-RYnTz49u4F5tDD2SUwwtlynABNBAfbyT2uU/brJcyh5k6lDLyNfYKdKmqd3K2ls4AaiALWrFKVSBsiVwhdFNzQ==}
|
||||
turbo-darwin-64@2.5.6:
|
||||
resolution: {integrity: sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
turbo-darwin-arm64@2.5.5:
|
||||
resolution: {integrity: sha512-Tk+ZeSNdBobZiMw9aFypQt0DlLsWSFWu1ymqsAdJLuPoAH05qCfYtRxE1pJuYHcJB5pqI+/HOxtJoQ40726Btw==}
|
||||
turbo-darwin-arm64@2.5.6:
|
||||
resolution: {integrity: sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
turbo-linux-64@2.5.5:
|
||||
resolution: {integrity: sha512-2/XvMGykD7VgsvWesZZYIIVXMlgBcQy+ZAryjugoTcvJv8TZzSU/B1nShcA7IAjZ0q7OsZ45uP2cOb8EgKT30w==}
|
||||
turbo-linux-64@2.5.6:
|
||||
resolution: {integrity: sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
turbo-linux-arm64@2.5.5:
|
||||
resolution: {integrity: sha512-DW+8CjCjybu0d7TFm9dovTTVg1VRnlkZ1rceO4zqsaLrit3DgHnN4to4uwyuf9s2V/BwS3IYcRy+HG9BL596Iw==}
|
||||
turbo-linux-arm64@2.5.6:
|
||||
resolution: {integrity: sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
turbo-windows-64@2.5.5:
|
||||
resolution: {integrity: sha512-q5p1BOy8ChtSZfULuF1BhFMYIx6bevXu4fJ+TE/hyNfyHJIfjl90Z6jWdqAlyaFLmn99X/uw+7d6T/Y/dr5JwQ==}
|
||||
turbo-windows-64@2.5.6:
|
||||
resolution: {integrity: sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
turbo-windows-arm64@2.5.5:
|
||||
resolution: {integrity: sha512-AXbF1KmpHUq3PKQwddMGoKMYhHsy5t1YBQO8HZ04HLMR0rWv9adYlQ8kaeQJTko1Ay1anOBFTqaxfVOOsu7+1Q==}
|
||||
turbo-windows-arm64@2.5.6:
|
||||
resolution: {integrity: sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
turbo@2.5.5:
|
||||
resolution: {integrity: sha512-eZ7wI6KjtT1eBqCnh2JPXWNUAxtoxxfi6VdBdZFvil0ychCOTxbm7YLRBi1JSt7U3c+u3CLxpoPxLdvr/Npr3A==}
|
||||
turbo@2.5.6:
|
||||
resolution: {integrity: sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w==}
|
||||
hasBin: true
|
||||
|
||||
type-check@0.4.0:
|
||||
@@ -13886,11 +13882,11 @@ snapshots:
|
||||
|
||||
'@chevrotain/utils@11.0.3': {}
|
||||
|
||||
'@clickhouse/client-common@1.12.0': {}
|
||||
'@clickhouse/client-common@1.12.1': {}
|
||||
|
||||
'@clickhouse/client@1.12.0':
|
||||
'@clickhouse/client@1.12.1':
|
||||
dependencies:
|
||||
'@clickhouse/client-common': 1.12.0
|
||||
'@clickhouse/client-common': 1.12.1
|
||||
|
||||
'@codemirror/autocomplete@6.17.0(@codemirror/language@6.11.2)(@codemirror/state@6.5.2)(@codemirror/view@6.38.1)(@lezer/common@1.2.3)':
|
||||
dependencies:
|
||||
@@ -19315,7 +19311,7 @@ snapshots:
|
||||
chokidar: 4.0.3
|
||||
confbox: 0.2.2
|
||||
defu: 6.1.4
|
||||
dotenv: 16.5.0
|
||||
dotenv: 16.6.1
|
||||
exsolve: 1.0.5
|
||||
giget: 2.0.0
|
||||
jiti: 2.4.2
|
||||
@@ -20264,14 +20260,12 @@ snapshots:
|
||||
|
||||
dotenv-expand@11.0.6:
|
||||
dependencies:
|
||||
dotenv: 16.4.5
|
||||
dotenv: 16.6.1
|
||||
|
||||
dotenv@16.0.3: {}
|
||||
|
||||
dotenv@16.4.5: {}
|
||||
|
||||
dotenv@16.5.0: {}
|
||||
|
||||
dotenv@16.6.1: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
@@ -20557,11 +20551,11 @@ snapshots:
|
||||
eslint-plugin-n: 16.6.2(eslint@8.57.0)
|
||||
eslint-plugin-promise: 6.4.0(eslint@8.57.0)
|
||||
|
||||
eslint-config-turbo@2.5.5(eslint@8.57.0)(turbo@2.5.5):
|
||||
eslint-config-turbo@2.5.6(eslint@8.57.0)(turbo@2.5.6):
|
||||
dependencies:
|
||||
eslint: 8.57.0
|
||||
eslint-plugin-turbo: 2.5.5(eslint@8.57.0)(turbo@2.5.5)
|
||||
turbo: 2.5.5
|
||||
eslint-plugin-turbo: 2.5.6(eslint@8.57.0)(turbo@2.5.6)
|
||||
turbo: 2.5.6
|
||||
|
||||
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)):
|
||||
dependencies:
|
||||
@@ -20806,11 +20800,11 @@ snapshots:
|
||||
'@microsoft/tsdoc': 0.14.2
|
||||
'@microsoft/tsdoc-config': 0.16.2
|
||||
|
||||
eslint-plugin-turbo@2.5.5(eslint@8.57.0)(turbo@2.5.5):
|
||||
eslint-plugin-turbo@2.5.6(eslint@8.57.0)(turbo@2.5.6):
|
||||
dependencies:
|
||||
dotenv: 16.0.3
|
||||
eslint: 8.57.0
|
||||
turbo: 2.5.5
|
||||
turbo: 2.5.6
|
||||
|
||||
eslint-plugin-unicorn@51.0.1(eslint@8.57.0):
|
||||
dependencies:
|
||||
@@ -25591,32 +25585,32 @@ snapshots:
|
||||
dependencies:
|
||||
fast-fifo: 1.3.2
|
||||
|
||||
turbo-darwin-64@2.5.5:
|
||||
turbo-darwin-64@2.5.6:
|
||||
optional: true
|
||||
|
||||
turbo-darwin-arm64@2.5.5:
|
||||
turbo-darwin-arm64@2.5.6:
|
||||
optional: true
|
||||
|
||||
turbo-linux-64@2.5.5:
|
||||
turbo-linux-64@2.5.6:
|
||||
optional: true
|
||||
|
||||
turbo-linux-arm64@2.5.5:
|
||||
turbo-linux-arm64@2.5.6:
|
||||
optional: true
|
||||
|
||||
turbo-windows-64@2.5.5:
|
||||
turbo-windows-64@2.5.6:
|
||||
optional: true
|
||||
|
||||
turbo-windows-arm64@2.5.5:
|
||||
turbo-windows-arm64@2.5.6:
|
||||
optional: true
|
||||
|
||||
turbo@2.5.5:
|
||||
turbo@2.5.6:
|
||||
optionalDependencies:
|
||||
turbo-darwin-64: 2.5.5
|
||||
turbo-darwin-arm64: 2.5.5
|
||||
turbo-linux-64: 2.5.5
|
||||
turbo-linux-arm64: 2.5.5
|
||||
turbo-windows-64: 2.5.5
|
||||
turbo-windows-arm64: 2.5.5
|
||||
turbo-darwin-64: 2.5.6
|
||||
turbo-darwin-arm64: 2.5.6
|
||||
turbo-linux-64: 2.5.6
|
||||
turbo-linux-arm64: 2.5.6
|
||||
turbo-windows-64: 2.5.6
|
||||
turbo-windows-arm64: 2.5.6
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ FROM --platform=${TARGETPLATFORM:-linux/amd64} node:20-alpine AS alpine
|
||||
RUN apk update && apk upgrade --no-cache libcrypto3 libssl3 libc6-compat busybox ssl_client
|
||||
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine AS base
|
||||
RUN npm install turbo@^2.5.5 --global
|
||||
RUN npm install turbo@^2.5.6 --global
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "3.97.2",
|
||||
"version": "3.99.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -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
|
||||
@@ -2328,6 +2374,54 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MembershipRequest'
|
||||
delete:
|
||||
description: >-
|
||||
Delete a membership from the organization associated with the API key
|
||||
(requires organization-scoped API key)
|
||||
operationId: organizations_deleteOrganizationMembership
|
||||
tags:
|
||||
- Organizations
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MembershipDeletionResponse'
|
||||
'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/DeleteMembershipRequest'
|
||||
/api/public/projects/{projectId}/memberships:
|
||||
get:
|
||||
description: >-
|
||||
@@ -2430,6 +2524,60 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MembershipRequest'
|
||||
delete:
|
||||
description: >-
|
||||
Delete a membership from a specific project (requires
|
||||
organization-scoped API key). The user must be a member of the
|
||||
organization.
|
||||
operationId: organizations_deleteProjectMembership
|
||||
tags:
|
||||
- Organizations
|
||||
parameters:
|
||||
- name: projectId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MembershipDeletionResponse'
|
||||
'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/DeleteMembershipRequest'
|
||||
/api/public/organizations/projects:
|
||||
get:
|
||||
description: >-
|
||||
@@ -3254,7 +3402,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 +4607,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 +6072,13 @@ components:
|
||||
- SPAN
|
||||
- GENERATION
|
||||
- EVENT
|
||||
- AGENT
|
||||
- TOOL
|
||||
- CHAIN
|
||||
- RETRIEVER
|
||||
- EVALUATOR
|
||||
- EMBEDDING
|
||||
- GUARDRAIL
|
||||
IngestionUsage:
|
||||
title: IngestionUsage
|
||||
oneOf:
|
||||
@@ -6845,6 +7016,14 @@ components:
|
||||
required:
|
||||
- userId
|
||||
- role
|
||||
DeleteMembershipRequest:
|
||||
title: DeleteMembershipRequest
|
||||
type: object
|
||||
properties:
|
||||
userId:
|
||||
type: string
|
||||
required:
|
||||
- userId
|
||||
MembershipResponse:
|
||||
title: MembershipResponse
|
||||
type: object
|
||||
@@ -6862,6 +7041,17 @@ components:
|
||||
- role
|
||||
- email
|
||||
- name
|
||||
MembershipDeletionResponse:
|
||||
title: MembershipDeletionResponse
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
userId:
|
||||
type: string
|
||||
required:
|
||||
- message
|
||||
- userId
|
||||
MembershipsResponse:
|
||||
title: MembershipsResponse
|
||||
type: object
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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) => {
|
||||
|
||||
@@ -281,6 +281,81 @@ describe("Memberships APIs", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/public/projects/[projectId]/memberships", () => {
|
||||
it("should delete an existing project membership with valid API key", async () => {
|
||||
// First ensure the user has an organization membership
|
||||
const orgMembership = await prisma.organizationMembership.upsert({
|
||||
where: {
|
||||
orgId_userId: {
|
||||
userId: testUserId,
|
||||
orgId: testOrgId,
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
userId: testUserId,
|
||||
orgId: testOrgId,
|
||||
role: Role.MEMBER,
|
||||
},
|
||||
});
|
||||
|
||||
// Create a project membership
|
||||
await prisma.projectMembership.upsert({
|
||||
where: {
|
||||
projectId_userId: {
|
||||
userId: testUserId,
|
||||
projectId: testProjectId,
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
userId: testUserId,
|
||||
projectId: testProjectId,
|
||||
role: Role.VIEWER,
|
||||
orgMembershipId: orgMembership.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Verify membership exists before deletion
|
||||
const membershipBefore = await prisma.projectMembership.findUnique({
|
||||
where: {
|
||||
projectId_userId: {
|
||||
userId: testUserId,
|
||||
projectId: testProjectId,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(membershipBefore).not.toBeNull();
|
||||
|
||||
// Delete the membership
|
||||
const response = await makeAPICall(
|
||||
"DELETE",
|
||||
`/api/public/projects/${testProjectId}/memberships`,
|
||||
{
|
||||
userId: testUserId,
|
||||
},
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBe(
|
||||
"Project membership deleted successfully",
|
||||
);
|
||||
expect(response.body.userId).toBe(testUserId);
|
||||
|
||||
// Verify the membership was deleted from the database
|
||||
const membershipAfter = await prisma.projectMembership.findUnique({
|
||||
where: {
|
||||
projectId_userId: {
|
||||
userId: testUserId,
|
||||
projectId: testProjectId,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(membershipAfter).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Organization Memberships", () => {
|
||||
@@ -404,5 +479,63 @@ describe("Memberships APIs", () => {
|
||||
expect(result.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/public/organizations/memberships", () => {
|
||||
it("should delete an existing organization membership with valid API key", async () => {
|
||||
// First ensure the membership exists
|
||||
await prisma.organizationMembership.upsert({
|
||||
where: {
|
||||
orgId_userId: {
|
||||
userId: testUserId,
|
||||
orgId: testOrgId,
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
userId: testUserId,
|
||||
orgId: testOrgId,
|
||||
role: Role.MEMBER,
|
||||
},
|
||||
});
|
||||
|
||||
// Verify membership exists before deletion
|
||||
const membershipBefore = await prisma.organizationMembership.findUnique(
|
||||
{
|
||||
where: {
|
||||
orgId_userId: {
|
||||
userId: testUserId,
|
||||
orgId: testOrgId,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(membershipBefore).not.toBeNull();
|
||||
|
||||
// Delete the membership
|
||||
const response = await makeAPICall(
|
||||
"DELETE",
|
||||
`/api/public/organizations/memberships`,
|
||||
{
|
||||
userId: testUserId,
|
||||
},
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBe("Membership deleted successfully");
|
||||
expect(response.body.userId).toBe(testUserId);
|
||||
|
||||
// Verify the membership was deleted from the database
|
||||
const membershipAfter = await prisma.organizationMembership.findUnique({
|
||||
where: {
|
||||
orgId_userId: {
|
||||
userId: testUserId,
|
||||
orgId: testOrgId,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(membershipAfter).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
|
||||
export const EnvLabel = ({ className }: { className?: string }) => {
|
||||
const [isHidden, setIsHidden] = useState(false);
|
||||
const session = useSession();
|
||||
if (!env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION) return null;
|
||||
if (!session.data?.user?.email?.endsWith("@langfuse.com")) return null;
|
||||
if (isHidden) return null;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 self-stretch whitespace-nowrap rounded-md px-1 py-0.5 text-xs",
|
||||
"flex cursor-pointer items-center gap-1 self-stretch whitespace-nowrap rounded-md px-1 py-0.5 text-xs",
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
|
||||
? "bg-light-blue text-dark-blue"
|
||||
: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV"
|
||||
@@ -17,6 +20,7 @@ export const EnvLabel = ({ className }: { className?: string }) => {
|
||||
: "bg-light-red text-dark-red",
|
||||
className,
|
||||
)}
|
||||
onClick={() => setIsHidden(true)}
|
||||
>
|
||||
{["EU", "US", "HIPAA"].includes(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION)
|
||||
? `PROD-${env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}`
|
||||
|
||||
@@ -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;
|
||||
@@ -99,6 +107,11 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const isProviderOptionsSupported = ![
|
||||
LLMAdapter.GoogleAIStudio,
|
||||
LLMAdapter.VertexAI,
|
||||
].includes(modelParams.adapter.value);
|
||||
|
||||
// Settings button component for reuse
|
||||
const SettingsButton = (
|
||||
<Popover open={modelSettingsOpen} onOpenChange={setModelSettingsOpen}>
|
||||
@@ -166,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>
|
||||
@@ -273,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>
|
||||
);
|
||||
@@ -484,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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -70,12 +70,19 @@ const DemoBadge = () => {
|
||||
const router = useRouter();
|
||||
const routerProjectId = router.query.projectId as string | undefined;
|
||||
|
||||
return env.NEXT_PUBLIC_DEMO_ORG_ID &&
|
||||
env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
routerProjectId === env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION) ? (
|
||||
<div className="flex border-b px-3 py-2">
|
||||
<Alert className="rounded-md bg-light-yellow group-data-[collapsible=icon]:hidden">
|
||||
if (
|
||||
!(
|
||||
env.NEXT_PUBLIC_DEMO_ORG_ID &&
|
||||
env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
routerProjectId === env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION)
|
||||
)
|
||||
)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div className="flex border-b px-3 py-2 group-data-[collapsible=icon]:hidden">
|
||||
<Alert className="rounded-md bg-light-yellow">
|
||||
<AlertDescription className="overflow-hidden text-ellipsis whitespace-nowrap text-xs">
|
||||
View-only{" "}
|
||||
<Link
|
||||
@@ -88,5 +95,5 @@ const DemoBadge = () => {
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ export function EvaluatorsOnboarding({ projectId }: EvaluatorsOnboardingProps) {
|
||||
}}
|
||||
secondaryAction={{
|
||||
label: "Learn More",
|
||||
href: "https://langfuse.com/docs/scores/model-based-evals",
|
||||
href: "https://langfuse.com/docs/evaluation/evaluation-methods/llm-as-a-judge",
|
||||
}}
|
||||
videoSrc="https://static.langfuse.com/prod-assets/onboarding/scores-llm-as-a-judge-overview-v1.mp4"
|
||||
/>
|
||||
|
||||
@@ -44,7 +44,7 @@ export function PromptsOnboarding({ projectId }: { projectId: string }) {
|
||||
}}
|
||||
secondaryAction={{
|
||||
label: "Learn More",
|
||||
href: "https://langfuse.com/docs/prompts",
|
||||
href: "https://langfuse.com/docs/prompt-management/get-started",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ export function ScoresOnboarding() {
|
||||
valuePropositions={valuePropositions}
|
||||
secondaryAction={{
|
||||
label: "Learn More",
|
||||
href: "https://langfuse.com/docs/scores",
|
||||
href: "https://langfuse.com/docs/evaluation/evaluation-methods/custom-scores",
|
||||
}}
|
||||
videoSrc="https://static.langfuse.com/prod-assets/onboarding/scores-overview-v1.mp4"
|
||||
/>
|
||||
|
||||
@@ -42,7 +42,7 @@ export function SessionsOnboarding() {
|
||||
To start using sessions, you need to add a `sessionId` to your traces.
|
||||
See{" "}
|
||||
<Link
|
||||
href="https://langfuse.com/docs/tracing-features/sessions"
|
||||
href="https://langfuse.com/docs/observability/features/sessions"
|
||||
className="underline"
|
||||
>
|
||||
documentation
|
||||
|
||||
@@ -48,7 +48,7 @@ export function TracesOnboarding({ projectId }: TracesOnboardingProps) {
|
||||
}}
|
||||
secondaryAction={{
|
||||
label: "View Documentation",
|
||||
href: "https://langfuse.com/docs/tracing",
|
||||
href: "https://langfuse.com/docs/observability/overview",
|
||||
}}
|
||||
videoSrc="https://static.langfuse.com/prod-assets/onboarding/tracing-overview-v1.mp4"
|
||||
/>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function UsersOnboarding() {
|
||||
To start tracking users, you need to add a `userId` to your traces.
|
||||
See{" "}
|
||||
<Link
|
||||
href="https://langfuse.com/docs/tracing-features/users"
|
||||
href="https://langfuse.com/docs/observability/features/users"
|
||||
className="underline"
|
||||
>
|
||||
documentation
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ResizableHandle,
|
||||
} from "@/src/components/ui/resizable";
|
||||
import { IOPreview } from "@/src/components/trace/IOPreview";
|
||||
import { ObservationTree } from "@/src/components/trace/ObservationTree";
|
||||
import { TraceTree } from "@/src/components/trace/TraceTree";
|
||||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
import { type RouterOutputs } from "@/src/utils/api";
|
||||
import { DatasetAggregateTableCell } from "@/src/features/datasets/components/DatasetAggregateTableCell";
|
||||
@@ -17,6 +17,8 @@ import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
import type { DatasetCompareRunRowData } from "@/src/features/datasets/components/DatasetCompareRunsTable";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePeekData } from "@/src/components/table/peek/hooks/usePeekData";
|
||||
import React, { useState, useCallback, useMemo } from "react";
|
||||
import { buildTraceTree } from "@/src/components/trace/lib/helpers";
|
||||
|
||||
export type PeekDatasetCompareDetailProps = {
|
||||
projectId: string;
|
||||
@@ -32,6 +34,7 @@ export const PeekDatasetCompareDetail = ({
|
||||
row,
|
||||
}: PeekDatasetCompareDetailProps) => {
|
||||
const router = useRouter();
|
||||
const [collapsedNodes, setCollapsedNodes] = useState<string[]>([]);
|
||||
|
||||
const timestamp =
|
||||
router.query.timestamp && typeof router.query.timestamp === "string"
|
||||
@@ -48,6 +51,37 @@ export const PeekDatasetCompareDetail = ({
|
||||
timestamp,
|
||||
});
|
||||
|
||||
const tree = useMemo(() => {
|
||||
if (!trace.data) return null;
|
||||
|
||||
const { tree } = buildTraceTree(
|
||||
trace.data,
|
||||
trace.data.observations ?? [],
|
||||
"DEFAULT",
|
||||
);
|
||||
|
||||
return tree;
|
||||
}, [trace.data]);
|
||||
|
||||
const toggleCollapsedNode = useCallback((id: string) => {
|
||||
setCollapsedNodes((prevNodes) => {
|
||||
if (prevNodes.includes(id)) {
|
||||
return prevNodes.filter((i) => i !== id);
|
||||
} else {
|
||||
return [...prevNodes, id];
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSetCurrentObservationId = (id?: string) => {
|
||||
if (id && traceId) {
|
||||
// Only open observations in new tabs; root selection passes undefined
|
||||
const pathname = `/project/${projectId}/traces/${encodeURIComponent(traceId)}?observation=${encodeURIComponent(id)}`;
|
||||
const pathnameWithBasePath = `${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}${pathname}`;
|
||||
window.open(pathnameWithBasePath, "_blank", "noopener noreferrer");
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleTrace = (
|
||||
newTraceId?: string,
|
||||
newRunId?: string,
|
||||
@@ -63,14 +97,7 @@ export const PeekDatasetCompareDetail = ({
|
||||
traceId: newTraceId,
|
||||
observationId: newObservationId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetCurrentObservationId = (id?: string) => {
|
||||
if (id && traceId) {
|
||||
const pathname = `/project/${projectId}/traces/${encodeURIComponent(traceId)}?observation=${encodeURIComponent(id)}`;
|
||||
const pathnameWithBasePath = `${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}${pathname}`;
|
||||
window.open(pathnameWithBasePath, "_blank", "noopener noreferrer");
|
||||
setCollapsedNodes([]); // Reset collapsed state for new trace
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,26 +114,24 @@ export const PeekDatasetCompareDetail = ({
|
||||
<div className="h-full overflow-y-auto border-r p-2">
|
||||
{trace.data ? (
|
||||
<Command>
|
||||
<h3 className="mb-3 font-semibold">{`Run: ${
|
||||
runsData?.find((r: any) => r.id === runId)?.name
|
||||
}`}</h3>
|
||||
<ObservationTree
|
||||
observations={trace.data?.observations ?? []}
|
||||
collapsedObservations={[]}
|
||||
toggleCollapsedObservation={() => {}}
|
||||
collapseAll={() => {}}
|
||||
expandAll={() => {}}
|
||||
trace={trace.data}
|
||||
scores={trace.data.scores ?? []}
|
||||
currentObservationId={undefined}
|
||||
setCurrentObservationId={handleSetCurrentObservationId}
|
||||
showComments={false}
|
||||
showMetrics={false}
|
||||
showScores={true}
|
||||
colorCodeMetrics={false}
|
||||
className="flex w-full flex-col overflow-y-auto"
|
||||
showExpandControls={false}
|
||||
/>
|
||||
<h3 className="mb-3 font-semibold">
|
||||
Run: {runsData?.find((r) => r.id === runId)?.name}
|
||||
</h3>
|
||||
{tree && (
|
||||
<TraceTree
|
||||
tree={tree}
|
||||
collapsedNodes={collapsedNodes}
|
||||
toggleCollapsedNode={toggleCollapsedNode}
|
||||
scores={trace.data?.scores ?? []}
|
||||
currentNodeId={undefined}
|
||||
setCurrentNodeId={handleSetCurrentObservationId}
|
||||
showMetrics={false}
|
||||
showScores={true}
|
||||
colorCodeMetrics={false}
|
||||
showComments={false}
|
||||
className="flex w-full flex-col overflow-y-auto"
|
||||
/>
|
||||
)}
|
||||
</Command>
|
||||
) : (
|
||||
<Skeleton className="min-h-full w-full" />
|
||||
@@ -152,7 +177,7 @@ export const PeekDatasetCompareDetail = ({
|
||||
{row?.runs && (
|
||||
<div className="flex h-[calc(100%-2rem)] w-full gap-4 overflow-x-auto">
|
||||
{Object.entries(row.runs).map(([id, run]) => {
|
||||
const runData = runsData?.find((r: any) => r.id === id);
|
||||
const runData = runsData?.find((r) => r.id === id);
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
@@ -190,7 +215,6 @@ export const PeekDatasetCompareDetail = ({
|
||||
>
|
||||
<ListTree className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
@@ -464,7 +473,7 @@ export default function ObservationsTable({
|
||||
headerTooltip: {
|
||||
description:
|
||||
"You can differentiate the importance of observations with the level attribute to control the verbosity of your traces and highlight errors and warnings.",
|
||||
href: "https://langfuse.com/docs/tracing-features/log-levels",
|
||||
href: "https://langfuse.com/docs/observability/features/log-levels",
|
||||
},
|
||||
enableHiding: true,
|
||||
cell({ row }) {
|
||||
@@ -491,7 +500,7 @@ export default function ObservationsTable({
|
||||
headerTooltip: {
|
||||
description:
|
||||
"Use a statusMessage to e.g. provide additional information on a status such as level=ERROR.",
|
||||
href: "https://langfuse.com/docs/tracing-features/log-levels",
|
||||
href: "https://langfuse.com/docs/observability/features/log-levels",
|
||||
},
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
@@ -626,7 +635,7 @@ export default function ObservationsTable({
|
||||
header: "Prompt",
|
||||
headerTooltip: {
|
||||
description: "Link to prompt version in Langfuse prompt management.",
|
||||
href: "https://langfuse.com/docs/prompts",
|
||||
href: "https://langfuse.com/docs/prompt-management/get-started",
|
||||
},
|
||||
size: 200,
|
||||
enableHiding: true,
|
||||
@@ -685,7 +694,7 @@ export default function ObservationsTable({
|
||||
size: 300,
|
||||
headerTooltip: {
|
||||
description: "Add metadata to traces to track additional information.",
|
||||
href: "https://langfuse.com/docs/tracing-features/metadata",
|
||||
href: "https://langfuse.com/docs/observability/features/metadata",
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const observationId: string = row.getValue("id");
|
||||
|
||||
@@ -368,7 +368,7 @@ export default function ScoresTable({
|
||||
id: "userId",
|
||||
headerTooltip: {
|
||||
description: "The user ID associated with the trace.",
|
||||
href: "https://langfuse.com/docs/tracing-features/users",
|
||||
href: "https://langfuse.com/docs/observability/features/users",
|
||||
},
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
@@ -437,7 +437,7 @@ export default function ScoresTable({
|
||||
headerTooltip: {
|
||||
description: "Add metadata to scores to track additional information.",
|
||||
// TODO: docs for metadata on scores
|
||||
href: "https://langfuse.com/docs/tracing-features/metadata",
|
||||
href: "https://langfuse.com/docs/observability/features/metadata",
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const scoreId: ScoresTableRow["id"] = row.getValue("id");
|
||||
@@ -493,7 +493,7 @@ export default function ScoresTable({
|
||||
id: "jobConfigurationId",
|
||||
headerTooltip: {
|
||||
description: "The Job Configuration ID associated with the trace.",
|
||||
href: "https://langfuse.com/docs/scores/model-based-evals",
|
||||
href: "https://langfuse.com/docs/evaluation/evaluation-methods/llm-as-a-judge",
|
||||
},
|
||||
enableHiding: true,
|
||||
enableSorting: false,
|
||||
|
||||
@@ -730,7 +730,7 @@ export default function SessionsTable({
|
||||
help={{
|
||||
description:
|
||||
"A session is a collection of related traces, such as a conversation or thread. To begin, add a sessionId to the trace.",
|
||||
href: "https://langfuse.com/docs/tracing-features/sessions",
|
||||
href: "https://langfuse.com/docs/observability/features/sessions",
|
||||
}}
|
||||
rowHeight={rowHeight}
|
||||
/>
|
||||
|
||||
@@ -669,7 +669,7 @@ export default function TracesTable({
|
||||
size: 150,
|
||||
headerTooltip: {
|
||||
description: "Group traces with tags.",
|
||||
href: "https://langfuse.com/docs/tracing-features/tags",
|
||||
href: "https://langfuse.com/docs/observability/features/tags",
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const tags: TracesTableRow["tags"] = row.getValue("tags");
|
||||
@@ -696,7 +696,7 @@ export default function TracesTable({
|
||||
size: 400,
|
||||
headerTooltip: {
|
||||
description: "Add metadata to traces to track additional information.",
|
||||
href: "https://langfuse.com/docs/tracing-features/metadata",
|
||||
href: "https://langfuse.com/docs/observability/features/metadata",
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const traceId: TracesTableRow["id"] = row.getValue("id");
|
||||
@@ -730,7 +730,7 @@ export default function TracesTable({
|
||||
size: 150,
|
||||
headerTooltip: {
|
||||
description: "Add `sessionId` to traces to track sessions.",
|
||||
href: "https://langfuse.com/docs/tracing-features/sessions",
|
||||
href: "https://langfuse.com/docs/observability/features/sessions",
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const value: TracesTableRow["sessionId"] = row.getValue("sessionId");
|
||||
@@ -749,7 +749,7 @@ export default function TracesTable({
|
||||
size: 150,
|
||||
headerTooltip: {
|
||||
description: "Add `userId` to traces to track users.",
|
||||
href: "https://langfuse.com/docs/tracing-features/users",
|
||||
href: "https://langfuse.com/docs/observability/features/users",
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const value: TracesTableRow["userId"] = row.getValue("userId");
|
||||
@@ -811,7 +811,7 @@ export default function TracesTable({
|
||||
size: 100,
|
||||
headerTooltip: {
|
||||
description: "Track changes via the version tag.",
|
||||
href: "https://langfuse.com/docs/experimentation",
|
||||
href: "https://langfuse.com/docs/observability/features/releases-and-versioning",
|
||||
},
|
||||
defaultHidden: true,
|
||||
enableHiding: true,
|
||||
@@ -824,7 +824,7 @@ export default function TracesTable({
|
||||
size: 100,
|
||||
headerTooltip: {
|
||||
description: "Track changes to your application via the release tag.",
|
||||
href: "https://langfuse.com/docs/experimentation",
|
||||
href: "https://langfuse.com/docs/observability/features/releases-and-versioning",
|
||||
},
|
||||
defaultHidden: true,
|
||||
enableHiding: true,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,530 +0,0 @@
|
||||
import { type NestedObservation } from "@/src/utils/types";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import {
|
||||
type APIScoreV2,
|
||||
ObservationLevel,
|
||||
type ObservationLevelType,
|
||||
type TraceDomain,
|
||||
} from "@langfuse/shared";
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { Fragment, useMemo, useRef, useEffect } from "react";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import { LevelColors } from "@/src/components/level-colors";
|
||||
import { formatIntervalSeconds } from "@/src/utils/dates";
|
||||
import {
|
||||
InfoIcon,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
ChevronsUpDown,
|
||||
ChevronsDownUp,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import {
|
||||
calculateDisplayTotalCost,
|
||||
heatMapTextColor,
|
||||
nestObservations,
|
||||
unnestObservation,
|
||||
} from "@/src/components/trace/lib/helpers";
|
||||
import { CommentCountIcon } from "@/src/features/comments/CommentCountIcon";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
import type Decimal from "decimal.js";
|
||||
import { CommandItem } from "@/src/components/ui/command";
|
||||
import { ItemBadge } from "@/src/components/ItemBadge";
|
||||
|
||||
export const ObservationTree = ({
|
||||
showExpandControls = true,
|
||||
showComments,
|
||||
...props
|
||||
}: {
|
||||
observations: ObservationReturnType[];
|
||||
collapsedObservations: string[];
|
||||
toggleCollapsedObservation: (id: string) => void;
|
||||
collapseAll: () => void;
|
||||
expandAll: () => void;
|
||||
trace: Omit<TraceDomain, "input" | "output" | "metadata"> & {
|
||||
latency?: number;
|
||||
input: string | null;
|
||||
output: string | null;
|
||||
metadata: string | null;
|
||||
};
|
||||
scores: APIScoreV2[];
|
||||
currentObservationId: string | undefined;
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
showMetrics: boolean;
|
||||
showScores: boolean;
|
||||
colorCodeMetrics: boolean;
|
||||
observationCommentCounts?: Map<string, number>;
|
||||
traceCommentCounts?: Map<string, number>;
|
||||
className?: string;
|
||||
showExpandControls?: boolean;
|
||||
minLevel?: ObservationLevelType;
|
||||
setMinLevel?: React.Dispatch<React.SetStateAction<ObservationLevelType>>;
|
||||
showComments: boolean;
|
||||
}) => {
|
||||
const { nestedObservations, hiddenObservationsCount } = useMemo(
|
||||
() => nestObservations(props.observations, props.minLevel),
|
||||
[props.observations, props.minLevel],
|
||||
);
|
||||
const totalCost = useMemo(() => {
|
||||
return calculateDisplayTotalCost({
|
||||
allObservations: props.observations,
|
||||
});
|
||||
}, [props.observations]);
|
||||
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<ObservationTreeTraceNode
|
||||
expandAll={props.expandAll}
|
||||
collapseAll={props.collapseAll}
|
||||
trace={props.trace}
|
||||
scores={props.scores}
|
||||
comments={props.traceCommentCounts}
|
||||
currentObservationId={props.currentObservationId}
|
||||
setCurrentObservationId={props.setCurrentObservationId}
|
||||
showMetrics={props.showMetrics}
|
||||
showScores={props.showScores}
|
||||
totalCost={totalCost}
|
||||
showExpandControls={showExpandControls}
|
||||
showComments={showComments}
|
||||
/>
|
||||
<ObservationTreeNode
|
||||
observations={nestedObservations}
|
||||
collapsedObservations={props.collapsedObservations}
|
||||
toggleCollapsedObservation={props.toggleCollapsedObservation}
|
||||
scores={props.scores}
|
||||
comments={props.observationCommentCounts}
|
||||
indentationLevel={1}
|
||||
currentObservationId={props.currentObservationId}
|
||||
setCurrentObservationId={props.setCurrentObservationId}
|
||||
showMetrics={props.showMetrics}
|
||||
showScores={props.showScores}
|
||||
colorCodeMetrics={props.colorCodeMetrics}
|
||||
parentTotalCost={totalCost}
|
||||
parentTotalDuration={
|
||||
props.trace.latency ? props.trace.latency * 1000 : undefined
|
||||
}
|
||||
showComments={showComments}
|
||||
/>
|
||||
{props.minLevel && hiddenObservationsCount > 0 ? (
|
||||
<span className="flex items-center gap-1 p-2 py-4">
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span>
|
||||
{hiddenObservationsCount}{" "}
|
||||
{hiddenObservationsCount === 1 ? "observation" : "observations"}{" "}
|
||||
below {props.minLevel} level are hidden.{" "}
|
||||
</span>
|
||||
<span
|
||||
className="cursor-pointer underline"
|
||||
onClick={() => props.setMinLevel?.(ObservationLevel.DEBUG)}
|
||||
>
|
||||
Show all
|
||||
</span>
|
||||
</p>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ObservationTreeTraceNode = (props: {
|
||||
trace: Omit<TraceDomain, "input" | "output" | "metadata"> & {
|
||||
input: string | null;
|
||||
output: string | null;
|
||||
metadata: string | null;
|
||||
latency?: number;
|
||||
};
|
||||
expandAll: () => void;
|
||||
collapseAll: () => void;
|
||||
scores: APIScoreV2[];
|
||||
comments: Map<string, number> | undefined;
|
||||
currentObservationId: string | undefined;
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
showMetrics?: boolean;
|
||||
showScores?: boolean;
|
||||
totalCost?: Decimal;
|
||||
showExpandControls?: boolean;
|
||||
showComments: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<CommandItem
|
||||
value={`${props.trace.name} TRACE`}
|
||||
className={cn(
|
||||
"flex w-full rounded-md px-0 hover:rounded-lg",
|
||||
(props.currentObservationId === undefined ||
|
||||
props.currentObservationId === "") &&
|
||||
"rounded-lg bg-muted",
|
||||
"after:hover:absolute after:hover:bottom-0 after:hover:left-0 after:hover:right-0 after:hover:h-[2px] after:hover:bg-background after:hover:content-['']",
|
||||
"before:hover:absolute before:hover:left-0 before:hover:right-0 before:hover:top-0 before:hover:h-[2px] before:hover:bg-background before:hover:content-['']",
|
||||
(props.currentObservationId === undefined ||
|
||||
props.currentObservationId === "") && [
|
||||
"after:absolute after:bottom-0 after:left-0 after:right-0 after:h-[2px] after:bg-background after:content-['']",
|
||||
"before:absolute before:left-0 before:right-0 before:top-0 before:h-[2px] before:bg-background before:content-['']",
|
||||
],
|
||||
)}
|
||||
style={{
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
cursor: "pointer",
|
||||
borderRadius: "0.5rem",
|
||||
}}
|
||||
onSelect={() => props.setCurrentObservationId(undefined)}
|
||||
>
|
||||
<div className="flex w-full flex-row items-start justify-between gap-1 py-1">
|
||||
<div className="flex w-full flex-col items-start gap-2 -space-y-1 py-1.5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<ItemBadge
|
||||
type="TRACE"
|
||||
isSmall={true}
|
||||
className="flex-shrink-0 scale-75"
|
||||
/>
|
||||
<span className="break-all text-sm font-medium">
|
||||
{props.trace.name}
|
||||
</span>
|
||||
{props.comments && props.showComments ? (
|
||||
<CommentCountIcon count={props.comments.get(props.trace.id)} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{props.showMetrics && (
|
||||
<div className="flex gap-2">
|
||||
{props.trace.latency ? (
|
||||
<span
|
||||
className="text-xs text-muted-foreground"
|
||||
title="Aggregated duration of all observations"
|
||||
>
|
||||
{formatIntervalSeconds(props.trace.latency)}
|
||||
</span>
|
||||
) : null}
|
||||
{props.totalCost ? (
|
||||
<span
|
||||
className="text-xs text-muted-foreground"
|
||||
title="Aggregated cost of all observations"
|
||||
>
|
||||
{usdFormatter(props.totalCost.toNumber())}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{props.showScores &&
|
||||
props.scores.find((s) => s.observationId === null) ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<GroupedScoreBadges
|
||||
scores={props.scores.filter((s) => s.observationId === null)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{props.showExpandControls && (
|
||||
<div className="mt-1 flex flex-1 items-center justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => props.expandAll()}
|
||||
className="h-4 w-4 flex-shrink-0"
|
||||
title="Expand all"
|
||||
>
|
||||
<ChevronsUpDown className="h-4 w-4 text-foreground" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => props.collapseAll()}
|
||||
className="h-4 w-4 flex-shrink-0"
|
||||
title="Collapse all"
|
||||
>
|
||||
<ChevronsDownUp className="h-4 w-4 text-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
};
|
||||
|
||||
const ObservationTreeNode = (props: {
|
||||
observations: NestedObservation[];
|
||||
collapsedObservations: string[];
|
||||
toggleCollapsedObservation: (id: string) => void;
|
||||
scores: APIScoreV2[];
|
||||
comments?: Map<string, number> | undefined;
|
||||
indentationLevel: number;
|
||||
currentObservationId: string | undefined;
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
showMetrics?: boolean;
|
||||
showScores?: boolean;
|
||||
colorCodeMetrics?: boolean;
|
||||
parentTotalCost?: Decimal;
|
||||
parentTotalDuration?: number;
|
||||
showComments: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col">
|
||||
{props.observations
|
||||
.sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
|
||||
.map((observation) => {
|
||||
const collapsed = props.collapsedObservations.includes(
|
||||
observation.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment key={observation.id}>
|
||||
<ObservationTreeNodeCard
|
||||
observation={observation}
|
||||
collapsed={collapsed}
|
||||
toggleCollapsedObservation={props.toggleCollapsedObservation}
|
||||
scores={props.scores}
|
||||
comments={props.comments}
|
||||
indentationLevel={props.indentationLevel}
|
||||
currentObservationId={props.currentObservationId}
|
||||
setCurrentObservationId={props.setCurrentObservationId}
|
||||
showMetrics={props.showMetrics}
|
||||
showScores={props.showScores}
|
||||
colorCodeMetrics={props.colorCodeMetrics}
|
||||
parentTotalCost={props.parentTotalCost}
|
||||
parentTotalDuration={props.parentTotalDuration}
|
||||
showComments={props.showComments}
|
||||
/>
|
||||
{!collapsed && observation.children.length > 0 && (
|
||||
<ObservationTreeNode
|
||||
{...props}
|
||||
observations={observation.children}
|
||||
indentationLevel={props.indentationLevel + 1}
|
||||
showComments={props.showComments}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ObservationTreeNodeCard = ({
|
||||
observation,
|
||||
collapsed,
|
||||
toggleCollapsedObservation,
|
||||
indentationLevel,
|
||||
currentObservationId,
|
||||
setCurrentObservationId,
|
||||
comments,
|
||||
showMetrics,
|
||||
showScores,
|
||||
scores,
|
||||
colorCodeMetrics,
|
||||
parentTotalCost,
|
||||
parentTotalDuration,
|
||||
showComments,
|
||||
}: {
|
||||
observation: NestedObservation;
|
||||
collapsed: boolean;
|
||||
toggleCollapsedObservation: (id: string) => void;
|
||||
scores: APIScoreV2[];
|
||||
comments?: Map<string, number> | undefined;
|
||||
indentationLevel: number;
|
||||
currentObservationId: string | undefined;
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
showMetrics?: boolean;
|
||||
showScores?: boolean;
|
||||
colorCodeMetrics?: boolean;
|
||||
parentTotalCost?: Decimal;
|
||||
parentTotalDuration?: number;
|
||||
showComments: boolean;
|
||||
}) => {
|
||||
const capture = usePostHogClientCapture();
|
||||
const unnestedObservations = unnestObservation(observation);
|
||||
const totalCost = calculateDisplayTotalCost({
|
||||
allObservations: unnestedObservations,
|
||||
});
|
||||
const duration = observation.endTime
|
||||
? observation.endTime.getTime() - observation.startTime.getTime()
|
||||
: undefined;
|
||||
|
||||
const currentObservationRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
currentObservationId &&
|
||||
currentObservationRef.current &&
|
||||
currentObservationId === observation.id
|
||||
) {
|
||||
currentObservationRef.current.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
}, [currentObservationId, observation.id]);
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
value={`${observation.name} ${observation.type} ${observation.id}`}
|
||||
className={cn(
|
||||
"relative flex w-full rounded-md px-0 hover:rounded-lg",
|
||||
currentObservationId === observation.id && "bg-muted",
|
||||
"after:hover:absolute after:hover:bottom-0 after:hover:left-0 after:hover:right-0 after:hover:h-[2px] after:hover:bg-background after:hover:content-['']",
|
||||
"before:hover:absolute before:hover:left-0 before:hover:right-0 before:hover:top-0 before:hover:h-[2px] before:hover:bg-background before:hover:content-['']",
|
||||
currentObservationId === observation.id && [
|
||||
"after:absolute after:bottom-0 after:left-0 after:right-0 after:h-[2px] after:bg-background after:content-['']",
|
||||
"before:absolute before:left-0 before:right-0 before:top-0 before:h-[2px] before:bg-background before:content-['']",
|
||||
],
|
||||
)}
|
||||
style={{
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
cursor: "pointer",
|
||||
borderRadius: "0.5rem",
|
||||
}}
|
||||
onSelect={() => setCurrentObservationId(observation.id)}
|
||||
>
|
||||
<div className="flex w-full">
|
||||
{/* Indentation guides */}
|
||||
{Array.from({ length: indentationLevel }, (_, i) => (
|
||||
<div className="w-6 flex-shrink-0 border-l border-border" key={i} />
|
||||
))}
|
||||
|
||||
{/* Node content */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-center gap-2 -space-y-1 py-2",
|
||||
)}
|
||||
ref={currentObservationRef}
|
||||
>
|
||||
{/* Type badge and name */}
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<ItemBadge
|
||||
type={observation.type}
|
||||
isSmall
|
||||
className="flex-shrink-0 scale-75"
|
||||
/>
|
||||
<span className="whitespace-nowrap text-sm font-medium">
|
||||
{observation.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Duration and Comments */}
|
||||
<div className="flex items-center gap-2">
|
||||
{comments && showComments ? (
|
||||
<CommentCountIcon count={comments.get(observation.id)} />
|
||||
) : null}
|
||||
{/* Level badge */}
|
||||
{observation.level !== "DEFAULT" ? (
|
||||
<div className="flex">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-sm p-0.5 text-xs",
|
||||
LevelColors[observation.level].bg,
|
||||
LevelColors[observation.level].text,
|
||||
)}
|
||||
>
|
||||
{observation.level}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{/* Metrics on their own line */}
|
||||
{showMetrics && (
|
||||
<>
|
||||
{(observation.inputUsage ||
|
||||
observation.outputUsage ||
|
||||
observation.totalUsage ||
|
||||
duration ||
|
||||
totalCost) && (
|
||||
<div className="flex w-full flex-wrap gap-2">
|
||||
{duration ? (
|
||||
<span
|
||||
title={
|
||||
observation.children.length > 0
|
||||
? "Aggregated duration of all child observations"
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
"text-xs text-muted-foreground",
|
||||
parentTotalDuration &&
|
||||
colorCodeMetrics &&
|
||||
heatMapTextColor({
|
||||
max: parentTotalDuration,
|
||||
value: duration,
|
||||
}),
|
||||
)}
|
||||
>
|
||||
{formatIntervalSeconds(duration / 1000)}
|
||||
</span>
|
||||
) : null}
|
||||
{observation.inputUsage ||
|
||||
observation.outputUsage ||
|
||||
observation.totalUsage ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{observation.inputUsage} → {observation.outputUsage} (∑{" "}
|
||||
{observation.totalUsage})
|
||||
</span>
|
||||
) : null}
|
||||
{totalCost ? (
|
||||
<span
|
||||
title={
|
||||
observation.children.length > 0
|
||||
? "Aggregated cost of all child observations"
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
"text-xs text-muted-foreground",
|
||||
parentTotalCost &&
|
||||
colorCodeMetrics &&
|
||||
heatMapTextColor({
|
||||
max: parentTotalCost,
|
||||
value: totalCost,
|
||||
}),
|
||||
)}
|
||||
>
|
||||
{observation.children.length > 0 ? "∑ " : ""}
|
||||
{usdFormatter(totalCost.toNumber())}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Scores on their own line */}
|
||||
{showScores &&
|
||||
scores.find((s) => s.observationId === observation.id) ? (
|
||||
<div className="flex w-full flex-wrap gap-1">
|
||||
<GroupedScoreBadges
|
||||
scores={scores.filter(
|
||||
(s) => s.observationId === observation.id,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{/* Expand/Collapse button */}
|
||||
|
||||
{observation.children.length > 0 && (
|
||||
<div className="flex items-center justify-end">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
toggleCollapsedObservation(observation.id);
|
||||
capture(
|
||||
collapsed
|
||||
? "trace_detail:observation_tree_expand"
|
||||
: "trace_detail:observation_tree_collapse",
|
||||
{ type: "single" },
|
||||
);
|
||||
}}
|
||||
className="h-full flex-shrink-0"
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -411,6 +411,7 @@ export function TraceTimelineView({
|
||||
colorCodeMetrics = true,
|
||||
minLevel,
|
||||
setMinLevel,
|
||||
containerWidth,
|
||||
}: {
|
||||
trace: Omit<TraceDomain, "input" | "output" | "metadata"> & {
|
||||
latency?: number;
|
||||
@@ -431,6 +432,7 @@ export function TraceTimelineView({
|
||||
colorCodeMetrics?: boolean;
|
||||
minLevel?: ObservationLevelType;
|
||||
setMinLevel?: React.Dispatch<React.SetStateAction<ObservationLevelType>>;
|
||||
containerWidth?: number;
|
||||
}) {
|
||||
const { latency, name, id } = trace;
|
||||
|
||||
@@ -439,6 +441,7 @@ export function TraceTimelineView({
|
||||
[observations, minLevel],
|
||||
);
|
||||
|
||||
// Use containerWidth from parent or fallback to ResizeObserver if not provided
|
||||
const [cardWidth, setCardWidth] = useState(0);
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -452,20 +455,33 @@ export function TraceTimelineView({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (containerWidth) {
|
||||
// Use passed container width from parent
|
||||
setCardWidth(containerWidth);
|
||||
} else {
|
||||
// Fallback to ResizeObserver if containerWidth not provided
|
||||
const handleResize = () => {
|
||||
if (parentRef.current) {
|
||||
const availableWidth = parentRef.current.offsetWidth;
|
||||
setCardWidth(availableWidth);
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
|
||||
if (parentRef.current) {
|
||||
const availableWidth = parentRef.current.offsetWidth;
|
||||
setCardWidth(availableWidth);
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
handleResize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(parentRef.current);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize); // Recalculate on window resize
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
}, [containerWidth]);
|
||||
|
||||
const isAuthenticatedAndProjectMember =
|
||||
useIsAuthenticatedAndProjectMember(projectId);
|
||||
@@ -532,7 +548,7 @@ export function TraceTimelineView({
|
||||
|
||||
return (
|
||||
<div ref={parentRef} className="h-full w-full px-3">
|
||||
<div className="relative flex max-h-full flex-col">
|
||||
<div className="relative flex h-full flex-col">
|
||||
{/* Sticky time index section - positioned absolutely at the top */}
|
||||
<div className="sticky top-0 z-20 bg-background">
|
||||
<div
|
||||
@@ -594,7 +610,7 @@ export function TraceTimelineView({
|
||||
{/* Main content with scrolling */}
|
||||
<div
|
||||
ref={outerContainerRef}
|
||||
className="overflow-x-auto"
|
||||
className="flex-1 overflow-x-auto"
|
||||
style={{ width: cardWidth }}
|
||||
onScroll={(e) => {
|
||||
if (timeIndexRef.current) {
|
||||
@@ -602,11 +618,11 @@ export function TraceTimelineView({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ width: `${contentWidth}px` }}>
|
||||
<div className="h-full" style={{ width: `${contentWidth}px` }}>
|
||||
{/* Main timeline content */}
|
||||
<div
|
||||
ref={timelineContentRef}
|
||||
className="overflow-y-auto"
|
||||
className="h-full overflow-y-auto"
|
||||
style={{ width: `${contentWidth}px` }}
|
||||
>
|
||||
<SimpleTreeView
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
import { type TreeNode } from "./lib/types";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import {
|
||||
type APIScoreV2,
|
||||
ObservationLevel,
|
||||
type ObservationLevelType,
|
||||
} from "@langfuse/shared";
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { Fragment, useMemo, useRef, useEffect } from "react";
|
||||
import { LevelColors } from "@/src/components/level-colors";
|
||||
import { formatIntervalSeconds } from "@/src/utils/dates";
|
||||
import { InfoIcon, ChevronRight } from "lucide-react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import {
|
||||
calculateDisplayTotalCost,
|
||||
heatMapTextColor,
|
||||
unnestObservation,
|
||||
} from "@/src/components/trace/lib/helpers";
|
||||
import { CommentCountIcon } from "@/src/features/comments/CommentCountIcon";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
import type Decimal from "decimal.js";
|
||||
import { CommandItem } from "@/src/components/ui/command";
|
||||
import { ItemBadge } from "@/src/components/ItemBadge";
|
||||
|
||||
export const TraceTree = ({
|
||||
tree,
|
||||
collapsedNodes,
|
||||
toggleCollapsedNode,
|
||||
scores,
|
||||
currentNodeId,
|
||||
setCurrentNodeId,
|
||||
showMetrics,
|
||||
showScores,
|
||||
colorCodeMetrics,
|
||||
nodeCommentCounts,
|
||||
className,
|
||||
showComments,
|
||||
hiddenObservationsCount,
|
||||
minLevel,
|
||||
setMinLevel,
|
||||
}: {
|
||||
tree: TreeNode;
|
||||
collapsedNodes: string[];
|
||||
toggleCollapsedNode: (id: string) => void;
|
||||
scores: APIScoreV2[];
|
||||
currentNodeId: string | undefined;
|
||||
setCurrentNodeId: (id: string | undefined) => void;
|
||||
showMetrics: boolean;
|
||||
showScores: boolean;
|
||||
colorCodeMetrics: boolean;
|
||||
nodeCommentCounts?: Map<string, number>;
|
||||
className?: string;
|
||||
showComments: boolean;
|
||||
hiddenObservationsCount?: number;
|
||||
minLevel?: ObservationLevelType;
|
||||
setMinLevel?: React.Dispatch<React.SetStateAction<ObservationLevelType>>;
|
||||
}) => {
|
||||
const totalCost = useMemo(() => {
|
||||
// For unified tree, we need to calculate total cost differently
|
||||
// Convert TreeNode back to observation format for cost calculation
|
||||
const convertTreeNodeToObservation = (node: TreeNode): any => ({
|
||||
...node,
|
||||
children: node.children.map(convertTreeNodeToObservation),
|
||||
});
|
||||
|
||||
if (tree.type === "TRACE") {
|
||||
// For trace root, calculate from all children
|
||||
const allObservations = tree.children.flatMap((child) =>
|
||||
unnestObservation(convertTreeNodeToObservation(child)),
|
||||
);
|
||||
return calculateDisplayTotalCost({ allObservations });
|
||||
}
|
||||
|
||||
return calculateDisplayTotalCost({
|
||||
allObservations: [convertTreeNodeToObservation(tree)],
|
||||
});
|
||||
}, [tree]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="pb-3">
|
||||
<TreeNodeComponent
|
||||
node={tree}
|
||||
collapsedNodes={collapsedNodes}
|
||||
toggleCollapsedNode={toggleCollapsedNode}
|
||||
scores={scores}
|
||||
comments={nodeCommentCounts}
|
||||
indentationLevel={0}
|
||||
currentNodeId={currentNodeId}
|
||||
setCurrentNodeId={setCurrentNodeId}
|
||||
showMetrics={showMetrics}
|
||||
showScores={showScores}
|
||||
colorCodeMetrics={colorCodeMetrics}
|
||||
parentTotalCost={totalCost}
|
||||
parentTotalDuration={tree.latency ? tree.latency * 1000 : undefined}
|
||||
showComments={showComments}
|
||||
treeLines={[]}
|
||||
isLastSibling={true}
|
||||
/>
|
||||
|
||||
{minLevel && hiddenObservationsCount && hiddenObservationsCount > 0 ? (
|
||||
<span className="flex items-center gap-1 p-2 py-4">
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span>
|
||||
{hiddenObservationsCount}{" "}
|
||||
{hiddenObservationsCount === 1 ? "observation" : "observations"}{" "}
|
||||
below {minLevel} level are hidden.{" "}
|
||||
</span>
|
||||
<span
|
||||
className="cursor-pointer underline"
|
||||
onClick={() => setMinLevel?.(ObservationLevel.DEBUG)}
|
||||
>
|
||||
Show all
|
||||
</span>
|
||||
</p>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TreeNodeComponent = ({
|
||||
node,
|
||||
collapsedNodes,
|
||||
toggleCollapsedNode,
|
||||
scores,
|
||||
comments,
|
||||
indentationLevel,
|
||||
currentNodeId,
|
||||
setCurrentNodeId,
|
||||
showMetrics,
|
||||
showScores,
|
||||
colorCodeMetrics,
|
||||
parentTotalCost,
|
||||
parentTotalDuration,
|
||||
showComments,
|
||||
treeLines,
|
||||
isLastSibling,
|
||||
}: {
|
||||
node: TreeNode;
|
||||
collapsedNodes: string[];
|
||||
toggleCollapsedNode: (id: string) => void;
|
||||
scores: APIScoreV2[];
|
||||
comments?: Map<string, number>;
|
||||
indentationLevel: number;
|
||||
currentNodeId: string | undefined;
|
||||
setCurrentNodeId: (id: string | undefined) => void;
|
||||
showMetrics: boolean;
|
||||
showScores: boolean;
|
||||
colorCodeMetrics: boolean;
|
||||
parentTotalCost?: Decimal;
|
||||
parentTotalDuration?: number;
|
||||
showComments: boolean;
|
||||
treeLines: boolean[]; // Track which levels need vertical lines
|
||||
isLastSibling: boolean;
|
||||
}) => {
|
||||
const capture = usePostHogClientCapture();
|
||||
const collapsed = collapsedNodes.includes(node.id);
|
||||
|
||||
// Convert TreeNode back to observation format for cost calculation
|
||||
const convertTreeNodeToObservation = (treeNode: TreeNode): any => ({
|
||||
...treeNode,
|
||||
children: treeNode.children.map(convertTreeNodeToObservation),
|
||||
});
|
||||
|
||||
const totalCost = calculateDisplayTotalCost({
|
||||
allObservations:
|
||||
node.children.length > 0
|
||||
? node.children.flatMap((child) =>
|
||||
unnestObservation(convertTreeNodeToObservation(child)),
|
||||
)
|
||||
: [convertTreeNodeToObservation(node)],
|
||||
});
|
||||
|
||||
const duration =
|
||||
node.endTime && node.startTime
|
||||
? node.endTime.getTime() - node.startTime.getTime()
|
||||
: node.latency
|
||||
? node.latency * 1000
|
||||
: undefined;
|
||||
|
||||
const currentNodeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentNodeId && currentNodeRef.current && currentNodeId === node.id) {
|
||||
currentNodeRef.current.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
}, [currentNodeId, node.id]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<CommandItem
|
||||
value={`${node.name} ${node.type} ${node.id}`}
|
||||
className={cn(
|
||||
"relative flex w-full rounded-md px-0 hover:rounded-lg",
|
||||
currentNodeId === node.id && "bg-muted/60 hover:bg-muted/60",
|
||||
)}
|
||||
style={{
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
cursor: "pointer",
|
||||
borderRadius: "0.5rem",
|
||||
}}
|
||||
onSelect={() =>
|
||||
setCurrentNodeId(node.type === "TRACE" ? undefined : node.id)
|
||||
}
|
||||
>
|
||||
<div className="flex w-full">
|
||||
{/* Tree structure indicators */}
|
||||
{indentationLevel > 0 && (
|
||||
<div className="flex flex-shrink-0">
|
||||
{/* Vertical lines for ancestor levels */}
|
||||
{Array.from({ length: indentationLevel - 1 }, (_, i) => (
|
||||
<div key={i} className="relative w-6">
|
||||
{treeLines[i] && (
|
||||
<div className="absolute bottom-0 left-3 top-0 w-px bg-border" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{/* Branch indicator for current level */}
|
||||
<div className="relative w-6">
|
||||
<div
|
||||
className="absolute left-3 z-10 w-px bg-border"
|
||||
style={{
|
||||
top: 0,
|
||||
bottom: isLastSibling ? "calc(100% - 12px)" : "12px",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute left-3 top-3 h-px w-3 bg-border" />
|
||||
{!isLastSibling && (
|
||||
<div className="absolute bottom-0 left-3 top-3 w-px bg-border" />
|
||||
)}
|
||||
{/* Downward connector for nodes with children */}
|
||||
{node.children.length > 0 && !collapsed && (
|
||||
<div
|
||||
className="absolute w-px bg-border"
|
||||
style={{ left: "36px", top: "18px", bottom: 0 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Downward connector for root nodes with children */}
|
||||
{indentationLevel === 0 && node.children.length > 0 && !collapsed && (
|
||||
<div
|
||||
className="absolute w-px bg-border"
|
||||
style={{ left: "20px", top: "18px", bottom: 0 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Node content */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-w-0 flex-1 items-start gap-2 py-1",
|
||||
currentNodeId !== node.id && "rounded-md hover:bg-muted/40",
|
||||
)}
|
||||
ref={currentNodeRef}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="relative z-20 flex-shrink-0">
|
||||
<ItemBadge type={node.type} isSmall className="scale-75" />
|
||||
</div>
|
||||
|
||||
{/* Content that can wrap */}
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
{/* First line: name, comments, level */}
|
||||
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||
<span
|
||||
className="flex-shrink truncate text-xs"
|
||||
title={node.name}
|
||||
>
|
||||
{node.name}
|
||||
</span>
|
||||
|
||||
{/* Comments and Level */}
|
||||
<div className="flex items-center gap-2">
|
||||
{comments && showComments ? (
|
||||
<CommentCountIcon count={comments.get(node.id)} />
|
||||
) : null}
|
||||
{/* Level badge (only for non-trace nodes) */}
|
||||
{node.type !== "TRACE" &&
|
||||
node.level &&
|
||||
node.level !== "DEFAULT" ? (
|
||||
<div className="flex">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-sm p-0.5 text-xs",
|
||||
LevelColors[node.level as keyof typeof LevelColors]
|
||||
?.bg,
|
||||
LevelColors[node.level as keyof typeof LevelColors]
|
||||
?.text,
|
||||
)}
|
||||
>
|
||||
{node.level}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics line */}
|
||||
{showMetrics &&
|
||||
(node.inputUsage ||
|
||||
node.outputUsage ||
|
||||
node.totalUsage ||
|
||||
duration ||
|
||||
totalCost ||
|
||||
node.latency) && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{duration || node.latency ? (
|
||||
<span
|
||||
title={
|
||||
node.children.length > 0 || node.type === "TRACE"
|
||||
? "Aggregated duration of all child observations"
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
"text-xs text-muted-foreground",
|
||||
parentTotalDuration &&
|
||||
colorCodeMetrics &&
|
||||
heatMapTextColor({
|
||||
max: parentTotalDuration,
|
||||
value:
|
||||
duration ||
|
||||
(node.latency ? node.latency * 1000 : 0),
|
||||
}),
|
||||
)}
|
||||
>
|
||||
{formatIntervalSeconds(
|
||||
(duration ||
|
||||
(node.latency ? node.latency * 1000 : 0)) / 1000,
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
{node.inputUsage || node.outputUsage || node.totalUsage ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{node.inputUsage} → {node.outputUsage} (∑{" "}
|
||||
{node.totalUsage})
|
||||
</span>
|
||||
) : null}
|
||||
{totalCost ? (
|
||||
<span
|
||||
title={
|
||||
node.children.length > 0 || node.type === "TRACE"
|
||||
? "Aggregated cost of all child observations"
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
"text-xs text-muted-foreground",
|
||||
parentTotalCost &&
|
||||
colorCodeMetrics &&
|
||||
heatMapTextColor({
|
||||
max: parentTotalCost,
|
||||
value: totalCost,
|
||||
}),
|
||||
)}
|
||||
>
|
||||
{node.children.length > 0 || node.type === "TRACE"
|
||||
? "∑ "
|
||||
: ""}
|
||||
{usdFormatter(totalCost.toNumber())}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Scores line */}
|
||||
{showScores &&
|
||||
((node.type === "TRACE" &&
|
||||
scores.find((s) => s.observationId === null)) ||
|
||||
scores.find((s) => s.observationId === node.id)) && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<GroupedScoreBadges
|
||||
scores={
|
||||
node.type === "TRACE"
|
||||
? scores.filter((s) => s.observationId === null)
|
||||
: scores.filter((s) => s.observationId === node.id)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expand/Collapse button */}
|
||||
{node.children.length > 0 && (
|
||||
<div className="flex items-center justify-end py-1">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
toggleCollapsedNode(node.id);
|
||||
capture(
|
||||
collapsed
|
||||
? "trace_detail:observation_tree_expand"
|
||||
: "trace_detail:observation_tree_collapse",
|
||||
{ type: "single", nodeType: node.type },
|
||||
);
|
||||
}}
|
||||
className="h-6 w-6 flex-shrink-0 hover:bg-primary/10"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"h-4 w-4 transition-transform duration-200",
|
||||
!collapsed && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
|
||||
{/* Render children */}
|
||||
{!collapsed && node.children.length > 0 && (
|
||||
<div className="flex w-full flex-col">
|
||||
{node.children
|
||||
.sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
|
||||
.map((childNode, index) => {
|
||||
const isChildLastSibling = index === node.children.length - 1;
|
||||
// Add to treeLines: whether there are more children after this one (determines if vertical line should continue)
|
||||
const childTreeLines = [...treeLines, !isChildLastSibling];
|
||||
|
||||
return (
|
||||
<TreeNodeComponent
|
||||
key={childNode.id}
|
||||
node={childNode}
|
||||
collapsedNodes={collapsedNodes}
|
||||
toggleCollapsedNode={toggleCollapsedNode}
|
||||
scores={scores}
|
||||
comments={comments}
|
||||
indentationLevel={indentationLevel + 1}
|
||||
currentNodeId={currentNodeId}
|
||||
setCurrentNodeId={setCurrentNodeId}
|
||||
showMetrics={showMetrics}
|
||||
showScores={showScores}
|
||||
colorCodeMetrics={colorCodeMetrics}
|
||||
parentTotalCost={parentTotalCost}
|
||||
parentTotalDuration={parentTotalDuration}
|
||||
showComments={showComments}
|
||||
treeLines={childTreeLines}
|
||||
isLastSibling={isChildLastSibling}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
useState,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import useLocalStorage from "@/src/components/useLocalStorage";
|
||||
|
||||
interface PanelState {
|
||||
sizes: number[]; // [leftPanelSize, rightPanelSize]
|
||||
minSize: number;
|
||||
maxSize: number;
|
||||
}
|
||||
|
||||
export function usePanelState(
|
||||
containerRef: React.RefObject<HTMLDivElement>,
|
||||
viewType: "timeline" | "tree",
|
||||
) {
|
||||
const previousViewTypeRef = useRef<string>(viewType);
|
||||
|
||||
const [timelineSizes, setTimelineSizes] = useLocalStorage(
|
||||
"trace-detail-timeline",
|
||||
[30, 70],
|
||||
);
|
||||
const [treeSizes, setTreeSizes] = useLocalStorage(
|
||||
"trace-detail-tree",
|
||||
[30, 70],
|
||||
);
|
||||
|
||||
const [panelState, setPanelState] = useState<PanelState>(() => {
|
||||
const savedSizes = viewType === "timeline" ? timelineSizes : treeSizes;
|
||||
|
||||
return {
|
||||
sizes: savedSizes,
|
||||
minSize: 25, // Will be updated by ResizeObserver
|
||||
maxSize: viewType === "timeline" ? 67 : 50, // Will be updated by ResizeObserver
|
||||
};
|
||||
});
|
||||
|
||||
// Handle view switching
|
||||
useEffect(() => {
|
||||
if (previousViewTypeRef.current !== viewType) {
|
||||
const newSizes = viewType === "timeline" ? timelineSizes : treeSizes;
|
||||
|
||||
setPanelState((prev) => ({
|
||||
...prev,
|
||||
sizes: newSizes,
|
||||
maxSize: viewType === "timeline" ? 67 : 50,
|
||||
}));
|
||||
|
||||
previousViewTypeRef.current = viewType;
|
||||
}
|
||||
}, [viewType, timelineSizes, treeSizes]);
|
||||
|
||||
const updateConstraints = useCallback(
|
||||
(containerWidth: number) => {
|
||||
const MIN_WIDTH_PX = 380;
|
||||
const MAX_TREE_WIDTH_PX = 500;
|
||||
|
||||
if (containerWidth <= 0) return;
|
||||
|
||||
const minSize = Math.max(
|
||||
10,
|
||||
Math.min(60, (MIN_WIDTH_PX / containerWidth) * 100),
|
||||
);
|
||||
const maxSize =
|
||||
viewType === "timeline"
|
||||
? 67
|
||||
: Math.max(25, (MAX_TREE_WIDTH_PX / containerWidth) * 100);
|
||||
|
||||
setPanelState((prev) => {
|
||||
if (prev.minSize !== minSize || prev.maxSize !== maxSize) {
|
||||
return { ...prev, minSize, maxSize };
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
},
|
||||
[viewType],
|
||||
);
|
||||
|
||||
const onLayout = useCallback(
|
||||
(sizes: number[]) => {
|
||||
if (viewType === "timeline") {
|
||||
setTimelineSizes(sizes);
|
||||
} else {
|
||||
setTreeSizes(sizes);
|
||||
}
|
||||
|
||||
setPanelState((prev) => ({ ...prev, sizes }));
|
||||
},
|
||||
[viewType, setTimelineSizes, setTreeSizes],
|
||||
);
|
||||
|
||||
// Handle container width changes
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry) {
|
||||
updateConstraints(entry.contentRect.width);
|
||||
}
|
||||
});
|
||||
|
||||
// Initial calculation
|
||||
updateConstraints(container.offsetWidth);
|
||||
|
||||
resizeObserver.observe(container);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [updateConstraints, containerRef]);
|
||||
|
||||
return {
|
||||
sizes: panelState.sizes,
|
||||
minSize: panelState.minSize,
|
||||
maxSize: panelState.maxSize,
|
||||
onLayout,
|
||||
};
|
||||
}
|
||||
+443
-335
@@ -1,5 +1,5 @@
|
||||
import { type ObservationLevelType, type TraceDomain } from "@langfuse/shared";
|
||||
import { ObservationTree } from "./ObservationTree";
|
||||
import { TraceTree } from "./TraceTree";
|
||||
import { ObservationPreview } from "./ObservationPreview";
|
||||
import { TracePreview } from "./TracePreview";
|
||||
import {
|
||||
@@ -12,11 +12,12 @@ import { api } from "@/src/utils/api";
|
||||
import useLocalStorage from "@/src/components/useLocalStorage";
|
||||
import {
|
||||
Settings2,
|
||||
ChevronsUpDown,
|
||||
ChevronsDownUp,
|
||||
Download,
|
||||
FoldVertical,
|
||||
UnfoldVertical,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useState, useMemo, useRef } from "react";
|
||||
import { usePanelState } from "./hooks/usePanelState";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { TraceTimelineView } from "@/src/components/trace/TraceTimelineView";
|
||||
import { type APIScoreV2, ObservationLevel } from "@langfuse/shared";
|
||||
@@ -35,11 +36,16 @@ import {
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuGroup,
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import useSessionStorage from "@/src/components/useSessionStorage";
|
||||
import { JsonExpansionProvider } from "@/src/components/trace/JsonExpansionContext";
|
||||
import { buildTraceTree } from "@/src/components/trace/lib/helpers";
|
||||
import {
|
||||
ResizablePanelGroup,
|
||||
ResizablePanel,
|
||||
ResizableHandle,
|
||||
} from "@/src/components/ui/resizable";
|
||||
|
||||
const getNestedObservationKeys = (
|
||||
observations: ObservationReturnTypeWithMetadata[],
|
||||
@@ -93,15 +99,20 @@ export function Trace(props: {
|
||||
] = useLocalStorage("colorCodeMetricsOnObservationTree", true);
|
||||
const [showComments, setShowComments] = useLocalStorage("showComments", true);
|
||||
const [showGraph, setShowGraph] = useLocalStorage("showGraph", true);
|
||||
const [collapsedObservations, setCollapsedObservations] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
const [collapsedNodes, setCollapsedNodes] = useState<string[]>([]);
|
||||
|
||||
const [minObservationLevel, setMinObservationLevel] =
|
||||
useState<ObservationLevelType>(
|
||||
props.defaultMinObservationLevel ?? ObservationLevel.DEFAULT,
|
||||
);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const panelState = usePanelState(
|
||||
containerRef,
|
||||
props.selectedTab?.includes("timeline") ? "timeline" : "tree",
|
||||
);
|
||||
|
||||
const isAuthenticatedAndProjectMember = useIsAuthenticatedAndProjectMember(
|
||||
props.projectId,
|
||||
);
|
||||
@@ -164,50 +175,20 @@ export function Trace(props: {
|
||||
const agentGraphData = agentGraphDataQuery.data ?? [];
|
||||
const isGraphViewAvailable = agentGraphData.length > 0;
|
||||
|
||||
const toggleCollapsedObservation = useCallback(
|
||||
const toggleCollapsedNode = useCallback(
|
||||
(id: string) => {
|
||||
if (collapsedObservations.includes(id)) {
|
||||
setCollapsedObservations(collapsedObservations.filter((i) => i !== id));
|
||||
if (collapsedNodes.includes(id)) {
|
||||
setCollapsedNodes(collapsedNodes.filter((i) => i !== id));
|
||||
} else {
|
||||
setCollapsedObservations([...collapsedObservations, id]);
|
||||
setCollapsedNodes([...collapsedNodes, id]);
|
||||
}
|
||||
},
|
||||
[collapsedObservations],
|
||||
[collapsedNodes],
|
||||
);
|
||||
|
||||
const collapseAll = useCallback(() => {
|
||||
// exclude all parents of the current observation
|
||||
let excludeParentObservations = new Set<string>();
|
||||
let newExcludeParentObservations = new Set<string>();
|
||||
do {
|
||||
excludeParentObservations = new Set<string>([
|
||||
...excludeParentObservations,
|
||||
...newExcludeParentObservations,
|
||||
]);
|
||||
newExcludeParentObservations = new Set<string>(
|
||||
props.observations
|
||||
.filter(
|
||||
(o) =>
|
||||
o.parentObservationId !== null &&
|
||||
(o.id === currentObservationId ||
|
||||
excludeParentObservations.has(o.id)),
|
||||
)
|
||||
.map((o) => o.parentObservationId as string)
|
||||
.filter((id) => !excludeParentObservations.has(id)),
|
||||
);
|
||||
} while (newExcludeParentObservations.size > 0);
|
||||
capture("trace_detail:observation_tree_collapse", { type: "all" });
|
||||
setCollapsedObservations(
|
||||
props.observations
|
||||
.map((o) => o.id)
|
||||
.filter((id) => !excludeParentObservations.has(id)),
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.observations, currentObservationId]);
|
||||
|
||||
const expandAll = useCallback(() => {
|
||||
capture("trace_detail:observation_tree_expand", { type: "all" });
|
||||
setCollapsedObservations([]);
|
||||
setCollapsedNodes([]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -237,211 +218,326 @@ export function Trace(props: {
|
||||
[`trace-${props.trace.id}`],
|
||||
);
|
||||
|
||||
// Build unified tree structure
|
||||
const { tree: traceTree, hiddenObservationsCount } = useMemo(
|
||||
() => buildTraceTree(props.trace, props.observations, minObservationLevel),
|
||||
[props.trace, props.observations, minObservationLevel],
|
||||
);
|
||||
|
||||
return (
|
||||
<JsonExpansionProvider>
|
||||
<div
|
||||
className={cn(
|
||||
"flex-1 gap-4 overflow-y-auto md:grid md:h-full md:grid-cols-5",
|
||||
props.selectedTab?.includes("timeline")
|
||||
? "md:grid-cols-[3fr_2fr] xl:grid-cols-[4fr_2fr]"
|
||||
: "md:grid-cols-[2fr_3fr] xl:grid-cols-[2fr_4fr]",
|
||||
)}
|
||||
ref={containerRef}
|
||||
className="flex-1 md:h-full"
|
||||
style={{ minWidth: "600px" }}
|
||||
>
|
||||
<div className="border-r md:flex md:h-full md:flex-col md:overflow-hidden">
|
||||
<Command className="mt-2 flex h-full flex-col gap-2 overflow-hidden rounded-none border-0">
|
||||
<div className="flex flex-row justify-between px-3 pl-5">
|
||||
{props.selectedTab?.includes("timeline") ? (
|
||||
<div className="flex h-full items-center gap-1">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setExpandedItems([
|
||||
`trace-${props.trace.id}`,
|
||||
...getNestedObservationKeys(props.observations),
|
||||
]);
|
||||
}}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
title="Expand all"
|
||||
className="px-0 text-muted-foreground"
|
||||
>
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setExpandedItems([])}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
title="Collapse all"
|
||||
className="px-0 text-muted-foreground"
|
||||
>
|
||||
<ChevronsDownUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="px-1 py-2 text-sm text-muted-foreground">
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="flex-1 md:h-full"
|
||||
onLayout={panelState.onLayout}
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={panelState.sizes[0]}
|
||||
minSize={panelState.minSize}
|
||||
maxSize={panelState.maxSize}
|
||||
className="md:flex md:h-full md:flex-col md:overflow-hidden"
|
||||
>
|
||||
<Command className="mt-2 flex h-full flex-col gap-2 overflow-hidden rounded-none border-0">
|
||||
<div className="flex flex-row justify-between px-3 pl-5">
|
||||
{props.selectedTab?.includes("timeline") ? (
|
||||
<span className="whitespace-nowrap px-1 py-2 text-sm text-muted-foreground">
|
||||
Node display
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<CommandInput
|
||||
showBorder={false}
|
||||
placeholder="Search (type, title, id)"
|
||||
className="-ml-2 h-9 border-0 focus:ring-0"
|
||||
/>
|
||||
)}
|
||||
{viewType === "detailed" && (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Settings2 className="h-4 w-4" />
|
||||
) : (
|
||||
<CommandInput
|
||||
showBorder={false}
|
||||
placeholder="Search"
|
||||
className="-ml-2 h-9 min-w-20 border-0 focus:ring-0"
|
||||
/>
|
||||
)}
|
||||
{viewType === "detailed" && (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{props.selectedTab?.includes("timeline") ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
// Check if trace is expanded (top level element)
|
||||
const isTraceExpanded = expandedItems.includes(
|
||||
`trace-${props.trace.id}`,
|
||||
);
|
||||
if (isTraceExpanded) {
|
||||
setExpandedItems([]);
|
||||
} else {
|
||||
setExpandedItems([
|
||||
`trace-${props.trace.id}`,
|
||||
...getNestedObservationKeys(props.observations),
|
||||
]);
|
||||
}
|
||||
}}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title={
|
||||
expandedItems.includes(`trace-${props.trace.id}`)
|
||||
? "Collapse all"
|
||||
: "Expand all"
|
||||
}
|
||||
>
|
||||
{expandedItems.includes(`trace-${props.trace.id}`) ? (
|
||||
<FoldVertical className="h-4 w-4" />
|
||||
) : (
|
||||
<UnfoldVertical className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Settings</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
) : (
|
||||
(() => {
|
||||
// Use the same root id format as the tree (see buildTraceTree)
|
||||
const traceRootId = `trace-${props.trace.id}`;
|
||||
// Check if everything is collapsed by seeing if the trace root is collapsed
|
||||
const isEverythingCollapsed =
|
||||
collapsedNodes.includes(traceRootId);
|
||||
|
||||
{isGraphViewAvailable && (
|
||||
<>
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (isEverythingCollapsed) {
|
||||
expandAll();
|
||||
} else {
|
||||
// Collapse all observations AND the trace root
|
||||
const allObservationIds =
|
||||
props.observations.map((o) => o.id);
|
||||
setCollapsedNodes([
|
||||
...allObservationIds,
|
||||
traceRootId,
|
||||
]);
|
||||
}
|
||||
}}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title={
|
||||
isEverythingCollapsed
|
||||
? "Expand all"
|
||||
: "Collapse all"
|
||||
}
|
||||
>
|
||||
{isEverythingCollapsed ? (
|
||||
<UnfoldVertical className="h-4 w-4" />
|
||||
) : (
|
||||
<FoldVertical className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="View Options"
|
||||
>
|
||||
<Settings2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel>View Options</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{isGraphViewAvailable && (
|
||||
<div className="p-1">
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="mr-2">Show Graph</span>
|
||||
<Switch
|
||||
checked={showGraph}
|
||||
onCheckedChange={(e) => setShowGraph(e)}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1 p-1">
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="mr-2">Show Graph</span>
|
||||
<span className="mr-2">Show Comments</span>
|
||||
<Switch
|
||||
checked={showGraph}
|
||||
onCheckedChange={(e) => setShowGraph(e)}
|
||||
checked={showComments}
|
||||
onCheckedChange={(e) => {
|
||||
setShowComments(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="mr-2">Show Comments</span>
|
||||
<Switch
|
||||
checked={showComments}
|
||||
onCheckedChange={(e) => {
|
||||
setShowComments(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="mr-2">Show Scores</span>
|
||||
<Switch
|
||||
checked={scoresOnObservationTree}
|
||||
onCheckedChange={(e) => {
|
||||
capture(
|
||||
"trace_detail:observation_tree_toggle_scores",
|
||||
{
|
||||
show: e,
|
||||
},
|
||||
);
|
||||
setScoresOnObservationTree(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="mr-2">Show Scores</span>
|
||||
<Switch
|
||||
checked={scoresOnObservationTree}
|
||||
onCheckedChange={(e) => {
|
||||
capture(
|
||||
"trace_detail:observation_tree_toggle_scores",
|
||||
{
|
||||
show: e,
|
||||
},
|
||||
);
|
||||
setScoresOnObservationTree(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="mr-2">Show Metrics</span>
|
||||
<Switch
|
||||
checked={metricsOnObservationTree}
|
||||
onCheckedChange={(e) => {
|
||||
capture(
|
||||
"trace_detail:observation_tree_toggle_metrics",
|
||||
{
|
||||
show: e,
|
||||
},
|
||||
);
|
||||
setMetricsOnObservationTree(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="mr-2">Show Metrics</span>
|
||||
<Switch
|
||||
checked={metricsOnObservationTree}
|
||||
onCheckedChange={(e) => {
|
||||
capture(
|
||||
"trace_detail:observation_tree_toggle_metrics",
|
||||
{
|
||||
show: e,
|
||||
},
|
||||
);
|
||||
setMetricsOnObservationTree(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="mr-2">Color Code Metrics</span>
|
||||
<Switch
|
||||
checked={colorCodeMetricsOnObservationTree}
|
||||
onCheckedChange={(e) =>
|
||||
setColorCodeMetricsOnObservationTree(e)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<span className="flex items-center">
|
||||
Min Level: {minObservationLevel}
|
||||
</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuLabel className="font-semibold">
|
||||
Minimum Level
|
||||
</DropdownMenuLabel>
|
||||
{Object.values(ObservationLevel).map((level) => (
|
||||
<DropdownMenuItem
|
||||
key={level}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setMinObservationLevel(level);
|
||||
}}
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
disabled={!metricsOnObservationTree}
|
||||
className={cn(
|
||||
!metricsOnObservationTree && "cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between",
|
||||
!metricsOnObservationTree &&
|
||||
"cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
{level}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={downloadTraceAsJson}
|
||||
title="Download trace as JSON"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
<Switch
|
||||
checked={props.selectedTab?.includes("timeline")}
|
||||
onCheckedChange={(checked) =>
|
||||
props.setSelectedTab?.(checked ? "timeline" : "preview")
|
||||
}
|
||||
></Switch>
|
||||
<span className="text-sm">Timeline</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-full overflow-hidden">
|
||||
{props.selectedTab?.includes("timeline") ? (
|
||||
<div className="h-full w-full flex-1 flex-col overflow-hidden">
|
||||
{isGraphViewAvailable && showGraph ? (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="h-1/2 w-full overflow-y-auto overflow-x-hidden">
|
||||
<span
|
||||
className={cn(
|
||||
"mr-2",
|
||||
!metricsOnObservationTree &&
|
||||
"cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
Color Code Metrics
|
||||
</span>
|
||||
<Switch
|
||||
checked={colorCodeMetricsOnObservationTree}
|
||||
onCheckedChange={(e) =>
|
||||
setColorCodeMetricsOnObservationTree(e)
|
||||
}
|
||||
disabled={!metricsOnObservationTree}
|
||||
className={cn(
|
||||
!metricsOnObservationTree &&
|
||||
"cursor-not-allowed",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<span className="flex items-center">
|
||||
Min Level: {minObservationLevel}
|
||||
</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuLabel className="font-semibold">
|
||||
Minimum Level
|
||||
</DropdownMenuLabel>
|
||||
{Object.values(ObservationLevel).map((level) => (
|
||||
<DropdownMenuItem
|
||||
key={level}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setMinObservationLevel(level);
|
||||
}}
|
||||
>
|
||||
{level}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={downloadTraceAsJson}
|
||||
title="Download trace as JSON"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<Switch
|
||||
checked={props.selectedTab?.includes("timeline")}
|
||||
onCheckedChange={(checked) =>
|
||||
props.setSelectedTab?.(
|
||||
checked ? "timeline" : "preview",
|
||||
"replaceIn",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<span className="text-sm">Timeline</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-full overflow-hidden">
|
||||
{props.selectedTab?.includes("timeline") ? (
|
||||
<div className="h-full w-full flex-1 flex-col overflow-hidden">
|
||||
{isGraphViewAvailable && showGraph ? (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="h-1/2 w-full overflow-y-auto overflow-x-hidden">
|
||||
<TraceTimelineView
|
||||
key={`timeline-${props.trace.id}`}
|
||||
trace={props.trace}
|
||||
scores={props.scores}
|
||||
observations={props.observations}
|
||||
projectId={props.trace.projectId}
|
||||
currentObservationId={currentObservationId ?? null}
|
||||
setCurrentObservationId={setCurrentObservationId}
|
||||
expandedItems={expandedItems}
|
||||
setExpandedItems={setExpandedItems}
|
||||
showMetrics={metricsOnObservationTree}
|
||||
showScores={scoresOnObservationTree}
|
||||
showComments={showComments}
|
||||
colorCodeMetrics={colorCodeMetricsOnObservationTree}
|
||||
minLevel={minObservationLevel}
|
||||
setMinLevel={setMinObservationLevel}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-1/2 w-full overflow-hidden border-t">
|
||||
<TraceGraphView
|
||||
key={`graph-timeline-${props.trace.id}`}
|
||||
agentGraphData={agentGraphData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full overflow-y-auto overflow-x-hidden">
|
||||
<TraceTimelineView
|
||||
key={`timeline-${props.trace.id}`}
|
||||
key={props.trace.id}
|
||||
trace={props.trace}
|
||||
scores={props.scores}
|
||||
observations={props.observations}
|
||||
@@ -458,127 +554,139 @@ export function Trace(props: {
|
||||
setMinLevel={setMinObservationLevel}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-1/2 w-full overflow-hidden border-t">
|
||||
<TraceGraphView
|
||||
key={`graph-timeline-${props.trace.id}`}
|
||||
agentGraphData={agentGraphData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full w-full flex-1 flex-col overflow-hidden">
|
||||
{isGraphViewAvailable && showGraph ? (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="h-1/2 w-full overflow-y-auto">
|
||||
<TraceTree
|
||||
tree={traceTree}
|
||||
collapsedNodes={collapsedNodes}
|
||||
toggleCollapsedNode={toggleCollapsedNode}
|
||||
scores={props.scores}
|
||||
currentNodeId={currentObservationId ?? undefined}
|
||||
setCurrentNodeId={setCurrentObservationId}
|
||||
showMetrics={metricsOnObservationTree}
|
||||
showScores={scoresOnObservationTree}
|
||||
showComments={showComments}
|
||||
colorCodeMetrics={colorCodeMetricsOnObservationTree}
|
||||
nodeCommentCounts={
|
||||
new Map(
|
||||
[
|
||||
...(observationCommentCounts.data
|
||||
? Array.from(
|
||||
observationCommentCounts.data.entries(),
|
||||
)
|
||||
: []),
|
||||
...(traceCommentCounts.data
|
||||
? [
|
||||
[
|
||||
props.trace.id,
|
||||
traceCommentCounts.data.get(
|
||||
props.trace.id,
|
||||
),
|
||||
],
|
||||
]
|
||||
: []),
|
||||
].filter(
|
||||
([, count]) => count !== undefined,
|
||||
) as [string, number][],
|
||||
)
|
||||
}
|
||||
className="flex w-full flex-col px-3"
|
||||
hiddenObservationsCount={hiddenObservationsCount}
|
||||
minLevel={minObservationLevel}
|
||||
setMinLevel={setMinObservationLevel}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-1/2 w-full overflow-hidden border-t">
|
||||
<TraceGraphView
|
||||
key={`graph-tree-${props.trace.id}`}
|
||||
agentGraphData={agentGraphData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full overflow-y-auto overflow-x-hidden">
|
||||
<TraceTimelineView
|
||||
key={props.trace.id}
|
||||
trace={props.trace}
|
||||
scores={props.scores}
|
||||
observations={props.observations}
|
||||
projectId={props.trace.projectId}
|
||||
currentObservationId={currentObservationId ?? null}
|
||||
setCurrentObservationId={setCurrentObservationId}
|
||||
expandedItems={expandedItems}
|
||||
setExpandedItems={setExpandedItems}
|
||||
showMetrics={metricsOnObservationTree}
|
||||
showScores={scoresOnObservationTree}
|
||||
showComments={showComments}
|
||||
colorCodeMetrics={colorCodeMetricsOnObservationTree}
|
||||
minLevel={minObservationLevel}
|
||||
setMinLevel={setMinObservationLevel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full w-full flex-1 flex-col overflow-hidden">
|
||||
{isGraphViewAvailable && showGraph ? (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="h-1/2 w-full overflow-y-auto">
|
||||
<ObservationTree
|
||||
observations={props.observations}
|
||||
collapsedObservations={collapsedObservations}
|
||||
toggleCollapsedObservation={
|
||||
toggleCollapsedObservation
|
||||
}
|
||||
collapseAll={collapseAll}
|
||||
expandAll={expandAll}
|
||||
trace={props.trace}
|
||||
) : (
|
||||
<div className="flex h-full w-full overflow-auto">
|
||||
<TraceTree
|
||||
tree={traceTree}
|
||||
collapsedNodes={collapsedNodes}
|
||||
toggleCollapsedNode={toggleCollapsedNode}
|
||||
scores={props.scores}
|
||||
currentObservationId={
|
||||
currentObservationId ?? undefined
|
||||
}
|
||||
setCurrentObservationId={setCurrentObservationId}
|
||||
currentNodeId={currentObservationId ?? undefined}
|
||||
setCurrentNodeId={setCurrentObservationId}
|
||||
showMetrics={metricsOnObservationTree}
|
||||
showScores={scoresOnObservationTree}
|
||||
showComments={showComments}
|
||||
colorCodeMetrics={colorCodeMetricsOnObservationTree}
|
||||
observationCommentCounts={
|
||||
observationCommentCounts.data
|
||||
nodeCommentCounts={
|
||||
new Map(
|
||||
[
|
||||
...(observationCommentCounts.data
|
||||
? Array.from(
|
||||
observationCommentCounts.data.entries(),
|
||||
)
|
||||
: []),
|
||||
...(traceCommentCounts.data
|
||||
? [
|
||||
[
|
||||
props.trace.id,
|
||||
traceCommentCounts.data.get(
|
||||
props.trace.id,
|
||||
),
|
||||
],
|
||||
]
|
||||
: []),
|
||||
].filter(([, count]) => count !== undefined) as [
|
||||
string,
|
||||
number,
|
||||
][],
|
||||
)
|
||||
}
|
||||
traceCommentCounts={traceCommentCounts.data}
|
||||
className="flex w-full flex-col px-3"
|
||||
hiddenObservationsCount={hiddenObservationsCount}
|
||||
minLevel={minObservationLevel}
|
||||
setMinLevel={setMinObservationLevel}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-1/2 w-full overflow-hidden border-t">
|
||||
<TraceGraphView
|
||||
key={`graph-tree-${props.trace.id}`}
|
||||
agentGraphData={agentGraphData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full overflow-auto">
|
||||
<ObservationTree
|
||||
observations={props.observations}
|
||||
collapsedObservations={collapsedObservations}
|
||||
toggleCollapsedObservation={toggleCollapsedObservation}
|
||||
collapseAll={collapseAll}
|
||||
expandAll={expandAll}
|
||||
trace={props.trace}
|
||||
scores={props.scores}
|
||||
currentObservationId={currentObservationId ?? undefined}
|
||||
setCurrentObservationId={setCurrentObservationId}
|
||||
showMetrics={metricsOnObservationTree}
|
||||
showScores={scoresOnObservationTree}
|
||||
showComments={showComments}
|
||||
colorCodeMetrics={colorCodeMetricsOnObservationTree}
|
||||
observationCommentCounts={observationCommentCounts.data}
|
||||
traceCommentCounts={traceCommentCounts.data}
|
||||
className="flex w-full flex-col px-3"
|
||||
minLevel={minObservationLevel}
|
||||
setMinLevel={setMinObservationLevel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Command>
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle className="relative w-px bg-border transition-colors duration-200 after:absolute after:inset-y-0 after:left-0 after:w-1 after:-translate-x-px after:bg-blue-200 after:opacity-0 after:transition-opacity after:duration-200 hover:after:opacity-100 data-[resize-handle-state='drag']:after:opacity-100" />
|
||||
|
||||
<ResizablePanel className="min-w-56 overflow-hidden md:h-full">
|
||||
<div className="h-full pl-3">
|
||||
{currentObservationId === undefined ||
|
||||
currentObservationId === "" ||
|
||||
currentObservationId === null ? (
|
||||
<TracePreview
|
||||
trace={props.trace}
|
||||
observations={props.observations}
|
||||
scores={props.scores}
|
||||
commentCounts={traceCommentCounts.data}
|
||||
viewType={viewType}
|
||||
/>
|
||||
) : isValidObservationId ? (
|
||||
<ObservationPreview
|
||||
observations={props.observations}
|
||||
scores={props.scores}
|
||||
projectId={props.projectId}
|
||||
currentObservationId={currentObservationId}
|
||||
traceId={props.trace.id}
|
||||
commentCounts={observationCommentCounts.data}
|
||||
viewType={viewType}
|
||||
isTimeline={props.selectedTab?.includes("timeline")}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</Command>
|
||||
</div>
|
||||
<div className="overflow-hidden pl-3 md:h-full md:p-0">
|
||||
{currentObservationId === undefined ||
|
||||
currentObservationId === "" ||
|
||||
currentObservationId === null ? (
|
||||
<TracePreview
|
||||
trace={props.trace}
|
||||
observations={props.observations}
|
||||
scores={props.scores}
|
||||
commentCounts={traceCommentCounts.data}
|
||||
viewType={viewType}
|
||||
/>
|
||||
) : isValidObservationId ? (
|
||||
<ObservationPreview
|
||||
observations={props.observations}
|
||||
scores={props.scores}
|
||||
projectId={props.projectId}
|
||||
currentObservationId={currentObservationId}
|
||||
traceId={props.trace.id}
|
||||
commentCounts={observationCommentCounts.data}
|
||||
viewType={viewType}
|
||||
isTimeline={props.selectedTab?.includes("timeline")}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</JsonExpansionProvider>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { type NestedObservation } from "@/src/utils/types";
|
||||
import { type TreeNode } from "./types";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import Decimal from "decimal.js";
|
||||
import {
|
||||
type ObservationType,
|
||||
type ObservationLevelType,
|
||||
ObservationLevel,
|
||||
type TraceDomain,
|
||||
} from "@langfuse/shared";
|
||||
|
||||
export type TreeItemType = ObservationType | "TRACE";
|
||||
@@ -191,3 +193,60 @@ export const unnestObservation = (nestedObservation: NestedObservation) => {
|
||||
});
|
||||
return unnestedObservations;
|
||||
};
|
||||
|
||||
// Transform trace + observations into unified tree structure
|
||||
export function buildTraceTree(
|
||||
trace: Omit<TraceDomain, "input" | "output" | "metadata"> & {
|
||||
input: string | null;
|
||||
output: string | null;
|
||||
metadata: string | null;
|
||||
latency?: number;
|
||||
},
|
||||
observations: ObservationReturnType[],
|
||||
minLevel?: ObservationLevelType,
|
||||
): {
|
||||
tree: TreeNode;
|
||||
hiddenObservationsCount: number;
|
||||
} {
|
||||
// First, nest the observations as before
|
||||
const { nestedObservations, hiddenObservationsCount } = nestObservations(
|
||||
observations,
|
||||
minLevel,
|
||||
);
|
||||
|
||||
// Convert observations to TreeNodes
|
||||
const convertObservationToTreeNode = (obs: NestedObservation): TreeNode => ({
|
||||
id: obs.id,
|
||||
type: obs.type,
|
||||
name: obs.name ?? "",
|
||||
startTime: obs.startTime,
|
||||
endTime: obs.endTime,
|
||||
level: obs.level,
|
||||
children: obs.children.map(convertObservationToTreeNode),
|
||||
inputUsage: obs.inputUsage,
|
||||
outputUsage: obs.outputUsage,
|
||||
totalUsage: obs.totalUsage,
|
||||
calculatedInputCost:
|
||||
"calculatedInputCost" in obs ? obs.calculatedInputCost : undefined,
|
||||
calculatedOutputCost:
|
||||
"calculatedOutputCost" in obs ? obs.calculatedOutputCost : undefined,
|
||||
calculatedTotalCost:
|
||||
"calculatedTotalCost" in obs ? obs.calculatedTotalCost : undefined,
|
||||
parentObservationId: obs.parentObservationId,
|
||||
traceId: obs.traceId,
|
||||
});
|
||||
|
||||
// Create the root tree node (trace)
|
||||
// Use a unique ID for the trace root to avoid conflicts with observations that might have the same ID
|
||||
const tree: TreeNode = {
|
||||
id: `trace-${trace.id}`,
|
||||
type: "TRACE",
|
||||
name: trace.name ?? "",
|
||||
startTime: trace.timestamp,
|
||||
endTime: null, // traces don't have explicit end times
|
||||
children: nestedObservations.map(convertObservationToTreeNode),
|
||||
latency: trace.latency,
|
||||
};
|
||||
|
||||
return { tree, hiddenObservationsCount };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { type ObservationType } from "@langfuse/shared";
|
||||
|
||||
// Unified tree node type for trace tree component
|
||||
export type TreeNode = {
|
||||
id: string;
|
||||
type: "TRACE" | ObservationType;
|
||||
name: string;
|
||||
startTime: Date;
|
||||
endTime?: Date | null;
|
||||
level?: string;
|
||||
children: TreeNode[];
|
||||
// Common properties
|
||||
inputUsage?: number | null;
|
||||
outputUsage?: number | null;
|
||||
totalUsage?: number | null;
|
||||
calculatedInputCost?: any;
|
||||
calculatedOutputCost?: any;
|
||||
calculatedTotalCost?: any;
|
||||
// Trace-specific properties (when type === 'TRACE')
|
||||
latency?: number;
|
||||
// Observation-specific properties (when type !== 'TRACE')
|
||||
parentObservationId?: string | null;
|
||||
traceId?: string;
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v3.97.2";
|
||||
export const VERSION = "v3.99.0";
|
||||
|
||||
@@ -9,6 +9,11 @@ const MembershipSchema = z.object({
|
||||
role: z.enum(Role),
|
||||
});
|
||||
|
||||
// Schema for delete request body validation
|
||||
const DeleteMembershipSchema = z.object({
|
||||
userId: z.string(),
|
||||
});
|
||||
|
||||
// GET - Retrieve all organization memberships
|
||||
export async function handleGetMemberships(
|
||||
req: NextApiRequest,
|
||||
@@ -92,3 +97,31 @@ export async function handleUpdateMembership(
|
||||
name: user.name,
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE - Remove an organization membership
|
||||
export async function handleDeleteMembership(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
orgId: string,
|
||||
) {
|
||||
const validatedBody = DeleteMembershipSchema.safeParse(req.body);
|
||||
if (!validatedBody.success) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid request body",
|
||||
details: validatedBody.error.issues,
|
||||
});
|
||||
}
|
||||
|
||||
// Delete the membership (using deleteMany to avoid errors on not found)
|
||||
await prisma.organizationMembership.deleteMany({
|
||||
where: {
|
||||
orgId,
|
||||
userId: validatedBody.data.userId,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
message: "Membership deleted successfully",
|
||||
userId: validatedBody.data.userId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ const MembershipSchema = z.object({
|
||||
role: z.enum(Role),
|
||||
});
|
||||
|
||||
// Schema for delete request body validation
|
||||
const DeleteMembershipSchema = z.object({
|
||||
userId: z.string(),
|
||||
});
|
||||
|
||||
// GET - Retrieve all project memberships
|
||||
export async function handleGetMemberships(
|
||||
req: NextApiRequest,
|
||||
@@ -109,3 +114,64 @@ export async function handleUpdateMembership(
|
||||
name: orgMembership.user.name,
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE - Remove a project membership
|
||||
export async function handleDeleteMembership(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
projectId: string,
|
||||
orgId: string,
|
||||
) {
|
||||
const validatedBody = DeleteMembershipSchema.safeParse(req.body);
|
||||
if (!validatedBody.success) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid request body",
|
||||
details: validatedBody.error.issues,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if membership exists
|
||||
const membership = await prisma.projectMembership.findUnique({
|
||||
where: {
|
||||
projectId_userId: {
|
||||
userId: validatedBody.data.userId,
|
||||
projectId: projectId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organizationMembership: {
|
||||
select: {
|
||||
orgId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
return res.status(404).json({
|
||||
error: "Project membership not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the membership belongs to the correct organization
|
||||
if (membership.organizationMembership.orgId !== orgId) {
|
||||
return res.status(403).json({
|
||||
error: "Project membership does not belong to this organization",
|
||||
});
|
||||
}
|
||||
|
||||
// Delete the membership
|
||||
await prisma.projectMembership.delete({
|
||||
where: {
|
||||
projectId_userId: {
|
||||
userId: validatedBody.data.userId,
|
||||
projectId: projectId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
message: "Project membership deleted successfully",
|
||||
userId: validatedBody.data.userId,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ export const env = createEnv({
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: z.enum(["true", "false"]).optional(),
|
||||
SALT: z.string({
|
||||
required_error:
|
||||
"A strong Salt is required to encrypt API keys securely. See: https://langfuse.com/docs/deployment/self-host#deploy-the-container",
|
||||
"A strong Salt is required to encrypt API keys securely. See: https://langfuse.com/self-hosting#deploy-the-container",
|
||||
}),
|
||||
// Add newly signed up users to default org and/or project with role
|
||||
LANGFUSE_DEFAULT_ORG_ID: z.string().optional(),
|
||||
|
||||
@@ -451,7 +451,7 @@ export function AnnotationQueueItemsTable({
|
||||
help={{
|
||||
description:
|
||||
"Add traces and/or observations to your annotation queue to have them annotated by your team across predefined dimensions.",
|
||||
href: "https://langfuse.com/docs/scores/model-based-evals",
|
||||
href: "https://langfuse.com/docs/evaluation/evaluation-methods/llm-as-a-judge",
|
||||
}}
|
||||
pagination={{
|
||||
totalCount: items.data?.totalItems ?? null,
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function AnnotationQueues() {
|
||||
help: {
|
||||
description:
|
||||
"Annotation queues are used to manage scoring workflows for your LLM projects. See docs to learn more.",
|
||||
href: "https://langfuse.com/docs/scores/annotation",
|
||||
href: "https://langfuse.com/docs/evaluation/evaluation-methods/annotation",
|
||||
},
|
||||
actionButtonsRight: (
|
||||
<CreateOrEditAnnotationQueueButton
|
||||
|
||||
@@ -99,7 +99,7 @@ export function ChartScores(props: {
|
||||
<NoDataOrLoading
|
||||
isLoading={props.isLoading || scores.isLoading}
|
||||
description="Scores evaluate LLM quality and can be created manually or using the SDK."
|
||||
href="https://langfuse.com/docs/scores"
|
||||
href="https://langfuse.com/docs/evaluation/overview"
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user