Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
487 changes: 487 additions & 0 deletions benchmarks/lib/langfuse-drafts.ts

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion benchmarks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"regrade": "tsx scripts/regrade-business.ts",
"judge": "tsx scripts/judge.ts",
"judge:batch": "node --import tsx scripts/judge-calibration-batch.ts",
"calibrate": "tsx scripts/calibrate.ts"
"calibrate": "tsx scripts/calibrate.ts",
"langfuse-drafts": "tsx scripts/langfuse-drafts.ts"
},
"devDependencies": {
"promptfoo": "0.124.1",
Expand Down
89 changes: 89 additions & 0 deletions benchmarks/scripts/langfuse-drafts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
collectLangfuseDraftBundle,
isSafeDraftEndpoint,
writeLangfuseDraftBundle,
} from '../lib/langfuse-drafts.js';

const repository = fileURLToPath(new URL('../../', import.meta.url));
const benchmarkDirectory = path.join(repository, 'benchmarks');

function option(args: string[], name: string): string | undefined {
const index = args.indexOf(name);
return index < 0 ? undefined : args[index + 1];
}

function parseDate(value: string | undefined, fallback: Date): Date {
if (value === undefined) return fallback;
const date = new Date(value);
if (!Number.isFinite(date.getTime()))
throw new Error('langfuse_time_range_invalid');
return date;
}

async function readCredentials() {
// runtime-config derives the main data directory from cwd, so select the
// repository before loading it. It reads encrypted local settings and then
// the existing COMMERCEAGENT_OTEL_* environment fallbacks.
process.chdir(repository);
const runtime = await import('../../src/runtime-config.js');
const settings = runtime.getSystemSettings();
const endpoint = settings.observabilityEndpoint;
const publicKey = settings.observabilityPublicKey;
const secretKey = runtime.getObservabilitySecret();
if (!endpoint || !publicKey || !secretKey)
throw new Error('langfuse_credentials_missing');
if (!isSafeDraftEndpoint(endpoint))
throw new Error('langfuse_endpoint_invalid');
return { endpoint, publicKey, secretKey };
}

export async function runLangfuseDraftExport(args = process.argv.slice(2)) {
if (args.includes('--help') || args.includes('-h')) {
process.stdout.write(
'Usage: npm run bench:langfuse-drafts -- [--from ISO] [--to ISO] [--min-score 4]\n' +
'Reads metadata-only failures and low numeric scores; writes skeleton drafts under benchmarks/results/langfuse-drafts/.\n',
);
return null;
}
const to = parseDate(option(args, '--to'), new Date());
const from = parseDate(
option(args, '--from'),
new Date(to.getTime() - 7 * 24 * 60 * 60 * 1_000),
);
const minScore = Number(option(args, '--min-score') ?? '4');
if (!Number.isFinite(minScore) || minScore <= 1 || minScore > 5)
throw new Error('langfuse_min_score_invalid');

const credentials = await readCredentials();
const bundle = await collectLangfuseDraftBundle({
...credentials,
from,
to,
minScore,
});
const outputDirectory = writeLangfuseDraftBundle(benchmarkDirectory, bundle);
const relativeOutput = path.relative(repository, outputDirectory);
process.stdout.write(
JSON.stringify({
...bundle.selection,
outputDirectory: relativeOutput,
automaticPromotion: false,
}) + '\n',
);
return { bundle, outputDirectory };
}

if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
)
try {
await runLangfuseDraftExport();
} catch (error) {
process.stderr.write(
`${error instanceof Error && /^[a-z0-9_:-]+$/i.test(error.message) ? error.message : 'langfuse_draft_export_failed'}\n`,
);
process.exitCode = 1;
}
9 changes: 9 additions & 0 deletions docs/ACL-MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ read-only 投影按 host-issued turn ID 精确匹配当前或已接纳的 queued
| 路由族/操作 | 权限 |
| ------------------------------------------------- | -------------------------------------------------- |
| Provider、系统容量、Host 集成、注册策略、系统外观 | `manage_system_config` |
| 查看 OpenTelemetry 配置 | `manage_system_config` |
| 修改 OpenTelemetry 配置和密钥 | admin + `manage_system_config` |
| Plugin Catalog 手动扫描 | admin / `manage_system_config` 路径中的 admin 检查 |
| 系统 MCP 写入 | admin |
| 用户创建、禁用、恢复、角色与权限 | `manage_users` |
Expand All @@ -216,6 +218,13 @@ read-only 投影按 host-issued turn ID 精确匹配当前或已接纳的 queued
系统 MCP 默认仅 admin 可用;只有显式设置为 shared 后,普通成员的 Agent 才能进入
有效能力清单。API 不回传 Secret 明文。

CommerceAgent 不提供 Langfuse Trace 查看接口。`manage_system_config` 可配置导出目标;
只有同时具备 `manage_system_config` 的管理员可以修改 OpenTelemetry 开关、地址和密钥;
具备该权限的用户可读取非 Secret 设置。Trace 查看由 Langfuse 项目自身的成员权限决定。
`user_id_hash` 和 `workspace_id_hash`
用于 Trace 搜索与过滤,不构成访问控制。部署者应限制 Langfuse 项目成员,避免未获授权的
用户查看其他用户的 Trace。

## 7. WebSocket

连接建立时把认证用户 ID、角色和 Permission 固定到 Session;每个操作仍重新检查
Expand Down
36 changes: 36 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,42 @@ Public:
`data/extra/` 和任务运行历史
- `POST /api/groups/:jid/reset-owner`,admin break-glass
- `GET /api/groups/:jid/messages`

## 系统设置与 OpenTelemetry

`GET|PUT /api/config/system` 受 `manage_system_config` 权限保护。OpenTelemetry 字段的
修改还要求调用者角色为 admin;其他系统配置仍按原权限策略处理。系统设置响应包含
`observabilityEnabled`、`observabilityEndpoint`、`observabilityPublicKey`、
`observabilitySampleRate`、`observabilityIncludeContent` 和只读的
`observabilityHasSecretKey`。写入请求可带 `observabilitySecretKey`;Secret Key
只写入本机 AES-256-GCM 加密文件,读取响应永远不会返回它。该设置遵循
Web 持久配置优先于 `COMMERCEAGENT_OTEL_*` 环境变量,环境变量优先于代码默认值。

默认关闭。开启后通过 OTLP/HTTP 向配置的 Langfuse 根地址发送 trace,路径为
`/api/public/otel/v1/traces`,使用 Basic Auth(用户名为 Public Key,密码为
Secret Key)。公网地址必须使用 HTTPS;HTTP 仅允许 `localhost` 和 `127.0.0.1`
用于本机调试。不要求特定部署形态,可使用 Langfuse 云版或自托管实例。

trace 记录运行 ID、轮次 ID、工作区与用户 ID 的 SHA-256 哈希、Profile 版本、模型名、
工具名、Token、耗时、错误类别和 `run_finished.reason`。每次**轮次**按采样率独立
抽样,同一会话的不同轮次可能有不同的采样结果。默认不导出正文、工具参数或工具返回。
只有显式开启 `observabilityIncludeContent` 后,才会导出经过脱敏的用户输入、助手输出、
工具输入和工具返回;API Key、Endpoint、常见第三方令牌、手机号、邮箱、身份证号、银行卡号、
IP 地址、地址、收件人/客户字段、Cookie 和令牌会在本机移除或替换后再进入 span。如果
脱敏后仍命中高风险模式,正文会降级为只发送原文长度和 SHA-256。正文脱敏是尽力而为,
不保证识别所有自由文本中的个人信息(例如未标注的人名);处理真实客户数据的生产环境
建议保持元数据模式,不开启正文上报。导出异步执行;导出失败不会影响 Agent 回复或
usage/计费记录,只增加进程内失败计数并输出一次不含端点、凭据和 payload 的分类日志。

CommerceAgent 不提供 Langfuse Trace 浏览路由。`manage_system_config` 用户可配置目标
地址和密钥;Trace 查看权限由目标 Langfuse 项目的成员权限控制。由于同一项目内的哈希
标识只用于筛选,不是授权边界,部署者应将项目查看权限限制给获准的运维人员。

Langfuse 可使用云版或自托管实例;CommerceAgent 只需要对应项目的 API 地址、Public Key
和 Secret Key。benchmark 的 `bench:langfuse-drafts` 命令是只读回流工具,只查询元数据与
评分字段,不读取 trace 正文、工具参数/返回或评分评论;输出仅为被 Git 忽略的空白结构草稿,
不会写入业务用例或 holdout。需要正文时,必须由人工在受控环境中另行补全并完成脱敏审核。

- `DELETE /api/groups/:jid/messages/:messageId`
- `GET|PUT /api/groups/:jid/env`
- `GET|PUT /api/groups/:jid/mcp`,仅兼容旧客户端
Expand Down
8 changes: 8 additions & 0 deletions docs/BENCHMARK-REVISION.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,3 +425,11 @@ snake_case、camelCase 和省略输入字段测试全部失败。
- 验证:Fake Messages 服务 6 种响应脚本、预算计算和选型门槛测试 8 项通过;68 条 reference/oracle 自检通过、932 个变异被拒绝;当前源码 Docker 镜像完整 E2E selftest 通过(1 次 attempt、2 次 fixture MCP 调用、测量有效、parity/reassessment/report/leak check 通过)。无真实模型调用、无 holdout 运行、无选型结论。
- 后续最终代码路径把 `bench:selftest` 改为驱动真实 Promptfoo Provider 与运行器,而非手工拼接 Promptfoo 结果;CI/本地 `make bench-selftest` 必须从当前 checkout 构建镜像,避免旧 latest 镜像的 entrypoint 漂移。完整 selftest 最终复跑通过,synthetic candidate 保持 `gateway_pending`,因此业务通过只验证链路,不计入模型质量。
- 最终验收:全量 `make test` 为 354 个测试文件、3162 项通过;benchmark typecheck、项目 typecheck、`make build`、`docs:check`、`format:check`、`git diff --check` 均通过。预算 CLI 已用历史 50-attempt run 验证输出 150 次筛选 attempts、CNY 14.13 估算和冷启动时间代理值。离线 full selftest 报告 1 次业务 attempt 测量有效、2 次 fixture MCP 成功调用、独立重评/报告/泄漏探针通过、真实模型调用 0、holdout 0。

### 阶段 3 Langfuse 元数据回流草稿(2026-10-24)

- 主 Agent `runAgent` 与 Conversation Agent 都通过 `withObservabilityCleanup` 在所有返回/异常路径收尾;每条已注册 run 在 `containerTimeout + 60s` 后还有超时关闭兜底,并清除 runId、chatJid、turnId 映射和定时器。新轮次遇到同 chat 的旧 trace 会先关闭旧 run,避免静默漏 trace。
- 新增只读 `bench:langfuse-drafts`:从系统设置或 `COMMERCEAGENT_OTEL_*` 环境回退读取 Langfuse 地址/Key;查询限时窗的 Observations API v2 `core,metadata` 与 Scores API v3 数值分数,不请求 input/output、状态文本、工具参数/返回、评分评论、模型名、Endpoint、用户或工作区标识。Trace ID 只在内存中关联,不写入草稿。
- 输出仅为 `benchmarks/results/langfuse-drafts/` 下 0700/0600 的空白结构草稿;Prompt、业务分类、风险等级、split、期望状态/事实/证据、工具计划和 rubric 均留空。脚本不读取或写入 cases/dev、cases/holdout,不触碰 holdout ledger,不提供自动提升或提交功能。草稿必须由人补全、通过 reference/oracle 自检和变异测试并确认后,才能手动进入回归用例。
- 本机 Langfuse Compose 的 Web 端口改映射到 `127.0.0.1:3100`,CommerceAgent 保持默认 3000;主系统设置地址同步到 3100,正文上报关闭。所有 Compose 宿主端口保持 loopback 绑定,历史联调 trace 仍为 0。
- 验证:Langfuse 草稿与 OTel 生命周期专项测试 16 项通过;benchmark typecheck、`make typecheck`、`make build`、`docs:check`、格式检查通过。全量测试沿用 4 workers 和临时权限镜像,358 个测试文件、3197 项通过。先以真实本机 Langfuse 空项目验证 CLI 为 0 observations/0 drafts;随后创建 1 条无正文合成失败 trace 和 1 条数值低分,CLI 产出 1 份骨架草稿,run/turn、终态、低分计数均匹配,traceId 和正文均未落盘。检查到兼容 API 在 `statusMessage`/`comment` 上可返回 null 占位;脚本允许 null/空值并仍拒绝非空内容。合成 trace、score 与输出目录已删除,查询确认剩余合成 observations/scores 均为 0。没有读取真实会话、写入 cases、读取 holdout 或调用外部模型。阶段 4 及之后未开始。
32 changes: 31 additions & 1 deletion docs/BENCHMARK.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,37 @@ Provider 导入无论成功或失败均立即删除 `provider-snapshot`。每个
Container 验收或模型质量结论。

阶段 4 的业务用例集已加入。阶段 5 已按真实 fixture 工具返回修正 Judge 输入,并完成相同样本诊断和 20 条新人工盲标。新样本在关键维度几乎全为高分,且每个关键维度都没有 3 条人工低分,样本信息不足;本轮 κ 不作报告,也不能据此判断 Judge 好坏或进入排名。阶段 6 只按确定性断言排名;Profile 对比和正式多候选运行尚未开始。最新结果见 [验证记录](BENCHMARK-VALIDATION.md)。
第一版不引入 Langfuse;线上观测仍需先定义脱敏和保留期限。

## Langfuse 失败回流草稿

CommerceAgent 的 Langfuse OTel 导出为独立 opt-in 设置;benchmark 默认使用隔离 `DATA_DIR`,
不读取或继承生产 Langfuse 配置,因此 `bench:run` 与 `bench:selftest` 不会向 Langfuse 写入 trace。
回流命令只读 Langfuse API,地址和密钥取自 CommerceAgent 系统设置,或由已有的
`COMMERCEAGENT_OTEL_ENDPOINT`、`COMMERCEAGENT_OTEL_PUBLIC_KEY`、
`COMMERCEAGENT_OTEL_SECRET_KEY` 环境回退配置提供;不要把密钥放进命令行参数。

```bash
npm run bench:langfuse-drafts -- --from 2026-10-17T00:00:00Z --to 2026-10-24T00:00:00Z --min-score 4
```

省略时间时查询最近 7 天;单次查询窗口最多 31 天。目标需支持 Langfuse Observations API v2
和 Scores API v3(Langfuse v4+)。脚本使用 Observations API 的 `core,metadata` 字段读取
`commerceagent.turn` 与 `commerceagent.tool`,并用 Scores API v3
识别低于阈值的数值评分。默认阈值为 4(1–5 评分中低于 4);只保留 run/turn ID、终态、
错误类别、工具名和低分条数用于选择与引用。查询不会请求 input/output、工具参数或返回、
状态文本、评分评论、模型名、用户/工作区标识或 Endpoint。trace ID 只在内存里关联观察和评分,
不会写入草稿。若兼容服务仍返回被禁止字段且其值非空,导出会中止;null 或空字符串占位会被
视为空值并忽略,绝不会写入草稿。

草稿写入被 `.gitignore` 排除的 `benchmarks/results/langfuse-drafts/`,文件权限为本机私有模式。
它只生成结构骨架:Prompt、业务分类、风险等级、split、期望状态、事实、证据、工具计划和
rubric 都留空,不推断预期答案,也不从回复正文补写内容。脚本不读取或写入
`benchmarks/cases/dev/`、`benchmarks/cases/holdout/`,不修改 holdout ledger、不自动提交,
也没有把草稿提升为正式用例的命令。

人工审核者必须在受控环境中补全合成或已批准的任务内容及风险等级、期望状态;真实会话正文
不能由此脚本导出。进入正式回归集前,每条草稿都必须通过固定参考答案自检、变异测试并由人
确认。holdout 只允许在最终正式运行中使用一次,回流和调试阶段不得读取或触碰 holdout。

## 开发验证

Expand Down
Loading
Loading