@agentproto/telemetry-langfuse 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.mjs +157 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeremy (agentik.net)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# `@agentproto/telemetry-langfuse`
|
|
2
|
+
|
|
3
|
+
A dependency-free sink that forwards agentproto eval events to the [Langfuse](https://langfuse.com) public ingestion REST API. No Langfuse SDK is required — the implementation uses `fetch` directly.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { langfuseTelemetry } from "@agentproto/telemetry-langfuse"
|
|
9
|
+
|
|
10
|
+
const sink = langfuseTelemetry({
|
|
11
|
+
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
|
12
|
+
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
|
13
|
+
baseUrl: "https://cloud.langfuse.com",
|
|
14
|
+
environment: "production",
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
runEval({ suite, sink })
|
|
18
|
+
|
|
19
|
+
await sink.flush()
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Event mapping
|
|
23
|
+
|
|
24
|
+
| EvalEvent kind | Langfuse action | Notes |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `eval.started` | `trace-create` | One trace per `runId`; name is `eval:${suiteId}`. |
|
|
27
|
+
| `eval.case.scored` | `score-create` | One score per case/scorer tuple. |
|
|
28
|
+
| `eval.finished` | `score-create` | Two scores: `eval.meanValue` and `eval.passRate`. |
|
|
29
|
+
| unknown | ignored | Forward compatibility. |
|
|
30
|
+
|
|
31
|
+
Events are buffered in memory until `flush()` POSTs them as a single batch to `POST ${baseUrl}/api/public/ingestion` with Basic auth.
|
|
32
|
+
|
|
33
|
+
## Security
|
|
34
|
+
|
|
35
|
+
Credentials are used only for Basic auth headers. They are never logged or returned from `flush()`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { EvalEvent } from '@agentproto/eval';
|
|
2
|
+
import { Telemetry } from '@agentproto/telemetry';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared Langfuse ingestion transport — auth, a batch buffer, and a flush that
|
|
6
|
+
* POSTs to `${baseUrl}/api/public/ingestion` with Basic auth. Both the eval
|
|
7
|
+
* sink (`langfuseTelemetry`) and the agent-session tracer map their own domain
|
|
8
|
+
* events into Langfuse batch items and hand them to this client; the client
|
|
9
|
+
* owns nothing domain-specific — just the wire.
|
|
10
|
+
*/
|
|
11
|
+
/** Credentials + endpoint for a Langfuse ingestion client. */
|
|
12
|
+
interface IngestionConfig {
|
|
13
|
+
/** Langfuse public key (Basic auth username). */
|
|
14
|
+
readonly publicKey: string;
|
|
15
|
+
/** Langfuse secret key (Basic auth password). */
|
|
16
|
+
readonly secretKey: string;
|
|
17
|
+
/** Langfuse base URL, e.g. `https://cloud.langfuse.com`. */
|
|
18
|
+
readonly baseUrl: string;
|
|
19
|
+
/** Optional fetch implementation (defaults to `globalThis.fetch`). */
|
|
20
|
+
readonly fetchImpl?: typeof fetch;
|
|
21
|
+
}
|
|
22
|
+
/** Result of a {@link IngestionClient.flush}. */
|
|
23
|
+
interface FlushResult {
|
|
24
|
+
/** HTTP status, or `0` when the batch was empty and no request was made. */
|
|
25
|
+
readonly status: number;
|
|
26
|
+
/** Number of batch items actually sent. */
|
|
27
|
+
readonly sent: number;
|
|
28
|
+
/** Parsed JSON response body (or the raw text if it wasn't JSON), `null` on an empty flush. */
|
|
29
|
+
readonly body: unknown;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* One Langfuse ingestion batch item. `id` is the batch-envelope id — Langfuse's
|
|
33
|
+
* idempotency/dedup key, which MUST be a unique string per operation (a create
|
|
34
|
+
* and a later update of the same object share a body id but need distinct
|
|
35
|
+
* envelope ids). `body` is any JSON object (a `trace-create`, `generation-create`,
|
|
36
|
+
* `span-update`, `score-create`, … payload).
|
|
37
|
+
*/
|
|
38
|
+
interface IngestionItem {
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly type: string;
|
|
41
|
+
readonly timestamp: string;
|
|
42
|
+
readonly body: object;
|
|
43
|
+
}
|
|
44
|
+
/** A batching Langfuse ingestion transport. */
|
|
45
|
+
interface IngestionClient {
|
|
46
|
+
/** Buffer one item for the next flush. */
|
|
47
|
+
enqueue(item: IngestionItem): void;
|
|
48
|
+
/** Number of buffered items not yet flushed. */
|
|
49
|
+
size(): number;
|
|
50
|
+
/** Drain the buffer and POST it. Atomic: items enqueued during the in-flight
|
|
51
|
+
* request stay buffered for the next flush rather than being dropped. */
|
|
52
|
+
flush(): Promise<FlushResult>;
|
|
53
|
+
}
|
|
54
|
+
/** Build a batching Langfuse ingestion client. */
|
|
55
|
+
declare function createIngestionClient(cfg: IngestionConfig): IngestionClient;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Dependency-free Langfuse ingestion REST API sink.
|
|
59
|
+
*
|
|
60
|
+
* Maps the agentproto {@link EvalEvent} stream into Langfuse `trace-create`
|
|
61
|
+
* and `score-create` objects and batches them for the public ingestion
|
|
62
|
+
* endpoint (`POST /api/public/ingestion`). No Langfuse SDK is used.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/** Configuration for the Langfuse telemetry sink. */
|
|
66
|
+
interface LangfuseTelemetryConfig {
|
|
67
|
+
/** Langfuse public key (used as the Basic auth username). */
|
|
68
|
+
readonly publicKey: string;
|
|
69
|
+
/** Langfuse secret key (used as the Basic auth password). */
|
|
70
|
+
readonly secretKey: string;
|
|
71
|
+
/** Langfuse base URL, e.g. `https://cloud.langfuse.com`. */
|
|
72
|
+
readonly baseUrl: string;
|
|
73
|
+
/** Optional environment label forwarded to Langfuse objects. */
|
|
74
|
+
readonly environment?: string;
|
|
75
|
+
/** Optional fetch implementation (defaults to `globalThis.fetch`). */
|
|
76
|
+
readonly fetchImpl?: typeof fetch;
|
|
77
|
+
}
|
|
78
|
+
/** Public type of the sink returned by {@link langfuseTelemetry}. */
|
|
79
|
+
type LangfuseTelemetrySink = Telemetry<EvalEvent> & {
|
|
80
|
+
flush(): Promise<FlushResult>;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Build a Langfuse ingestion sink for {@link EvalEvent}s.
|
|
84
|
+
*
|
|
85
|
+
* `emit()` buffers events into a batch. `flush()` POSTs the batch to
|
|
86
|
+
* `${cfg.baseUrl}/api/public/ingestion` with Basic auth and returns the
|
|
87
|
+
* HTTP status plus parsed response body.
|
|
88
|
+
*/
|
|
89
|
+
declare function langfuseTelemetry(cfg: LangfuseTelemetryConfig): LangfuseTelemetrySink;
|
|
90
|
+
|
|
91
|
+
export { type FlushResult, type IngestionClient, type IngestionConfig, type IngestionItem, type LangfuseTelemetryConfig, type LangfuseTelemetrySink, createIngestionClient, langfuseTelemetry };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agentproto/telemetry-langfuse v0.1.0
|
|
3
|
+
* Dependency-free Langfuse ingestion REST API sink for agentproto eval events.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// src/ingestion.ts
|
|
7
|
+
function createIngestionClient(cfg) {
|
|
8
|
+
const fetchImpl = cfg.fetchImpl ?? globalThis.fetch;
|
|
9
|
+
const auth = `Basic ${Buffer.from(`${cfg.publicKey}:${cfg.secretKey}`).toString("base64")}`;
|
|
10
|
+
const url = `${cfg.baseUrl}/api/public/ingestion`;
|
|
11
|
+
const batch = [];
|
|
12
|
+
return {
|
|
13
|
+
enqueue(item) {
|
|
14
|
+
batch.push(item);
|
|
15
|
+
},
|
|
16
|
+
size() {
|
|
17
|
+
return batch.length;
|
|
18
|
+
},
|
|
19
|
+
async flush() {
|
|
20
|
+
const items = batch.splice(0, batch.length);
|
|
21
|
+
if (items.length === 0) {
|
|
22
|
+
return { status: 0, sent: 0, body: null };
|
|
23
|
+
}
|
|
24
|
+
const response = await fetchImpl(url, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
Authorization: auth
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ batch: items })
|
|
31
|
+
});
|
|
32
|
+
const text = await response.text();
|
|
33
|
+
let body;
|
|
34
|
+
try {
|
|
35
|
+
body = JSON.parse(text);
|
|
36
|
+
} catch {
|
|
37
|
+
body = text;
|
|
38
|
+
}
|
|
39
|
+
return { status: response.status, sent: items.length, body };
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/langfuse.ts
|
|
45
|
+
function langfuseTelemetry(cfg) {
|
|
46
|
+
const client = createIngestionClient(cfg);
|
|
47
|
+
const tracedRuns = /* @__PURE__ */ new Set();
|
|
48
|
+
const caseSpanId = (runId, caseId) => `${runId}:case:${caseId}`;
|
|
49
|
+
function withEnv(body) {
|
|
50
|
+
if (cfg.environment !== void 0) return { ...body, environment: cfg.environment };
|
|
51
|
+
return body;
|
|
52
|
+
}
|
|
53
|
+
function pushTrace(op, timestamp, body) {
|
|
54
|
+
client.enqueue({ id: `${body.id}#${op}`, type: "trace-create", timestamp, body: withEnv(body) });
|
|
55
|
+
}
|
|
56
|
+
function pushScore(timestamp, body) {
|
|
57
|
+
client.enqueue({ id: `${body.id}#score`, type: "score-create", timestamp, body: withEnv(body) });
|
|
58
|
+
}
|
|
59
|
+
function pushSpanCreate(timestamp, body) {
|
|
60
|
+
client.enqueue({ id: `${body.id}#span-create`, type: "span-create", timestamp, body: withEnv(body) });
|
|
61
|
+
}
|
|
62
|
+
function pushSpanUpdate(timestamp, body) {
|
|
63
|
+
client.enqueue({ id: `${body.id}#span-update`, type: "span-update", timestamp, body: withEnv(body) });
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
emit(event) {
|
|
67
|
+
switch (event.kind) {
|
|
68
|
+
case "eval.started": {
|
|
69
|
+
if (tracedRuns.has(event.runId)) return;
|
|
70
|
+
tracedRuns.add(event.runId);
|
|
71
|
+
pushTrace("create", event.at, {
|
|
72
|
+
id: event.runId,
|
|
73
|
+
name: `eval:${event.suiteId}`,
|
|
74
|
+
timestamp: event.at,
|
|
75
|
+
input: {
|
|
76
|
+
suiteId: event.suiteId,
|
|
77
|
+
caseCount: event.caseCount,
|
|
78
|
+
scorerCount: event.scorerCount
|
|
79
|
+
},
|
|
80
|
+
metadata: {
|
|
81
|
+
caseCount: event.caseCount,
|
|
82
|
+
scorerCount: event.scorerCount
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "eval.case.started": {
|
|
88
|
+
pushSpanCreate(event.at, {
|
|
89
|
+
id: caseSpanId(event.runId, event.caseId),
|
|
90
|
+
traceId: event.runId,
|
|
91
|
+
name: `case:${event.caseId}`,
|
|
92
|
+
startTime: event.at,
|
|
93
|
+
input: { caseId: event.caseId }
|
|
94
|
+
});
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case "eval.case.scored": {
|
|
98
|
+
pushScore(event.at, {
|
|
99
|
+
id: `${event.runId}:${event.caseId}:${event.scorerId}`,
|
|
100
|
+
traceId: event.runId,
|
|
101
|
+
name: event.scorerId,
|
|
102
|
+
value: event.value,
|
|
103
|
+
dataType: "NUMERIC",
|
|
104
|
+
comment: `case=${event.caseId} passed=${event.passed}`,
|
|
105
|
+
observationId: caseSpanId(event.runId, event.caseId),
|
|
106
|
+
timestamp: event.at
|
|
107
|
+
});
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "eval.case.finished": {
|
|
111
|
+
pushSpanUpdate(event.at, {
|
|
112
|
+
id: caseSpanId(event.runId, event.caseId),
|
|
113
|
+
traceId: event.runId,
|
|
114
|
+
endTime: event.at,
|
|
115
|
+
output: { passed: event.passed }
|
|
116
|
+
});
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case "eval.finished": {
|
|
120
|
+
const passRate = event.total > 0 ? event.passedCount / event.total : 0;
|
|
121
|
+
pushTrace("output", event.at, {
|
|
122
|
+
id: event.runId,
|
|
123
|
+
name: `eval:${event.suiteId}`,
|
|
124
|
+
timestamp: event.at,
|
|
125
|
+
output: {
|
|
126
|
+
total: event.total,
|
|
127
|
+
passedCount: event.passedCount,
|
|
128
|
+
failedCount: event.total - event.passedCount,
|
|
129
|
+
meanValue: event.meanValue,
|
|
130
|
+
passRate,
|
|
131
|
+
durationMs: event.durationMs
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
for (const name of ["eval.meanValue", "eval.passRate"]) {
|
|
135
|
+
const value = name === "eval.meanValue" ? event.meanValue : passRate;
|
|
136
|
+
pushScore(event.at, {
|
|
137
|
+
id: `${event.runId}:${name}`,
|
|
138
|
+
traceId: event.runId,
|
|
139
|
+
name,
|
|
140
|
+
value,
|
|
141
|
+
dataType: "NUMERIC",
|
|
142
|
+
timestamp: event.at
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
flush() {
|
|
150
|
+
return client.flush();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export { createIngestionClient, langfuseTelemetry };
|
|
156
|
+
//# sourceMappingURL=index.mjs.map
|
|
157
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ingestion.ts","../src/langfuse.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Shared Langfuse ingestion transport — auth, a batch buffer, and a flush that\n * POSTs to `${baseUrl}/api/public/ingestion` with Basic auth. Both the eval\n * sink (`langfuseTelemetry`) and the agent-session tracer map their own domain\n * events into Langfuse batch items and hand them to this client; the client\n * owns nothing domain-specific — just the wire.\n */\n\n/** Credentials + endpoint for a Langfuse ingestion client. */\nexport interface IngestionConfig {\n /** Langfuse public key (Basic auth username). */\n readonly publicKey: string\n /** Langfuse secret key (Basic auth password). */\n readonly secretKey: string\n /** Langfuse base URL, e.g. `https://cloud.langfuse.com`. */\n readonly baseUrl: string\n /** Optional fetch implementation (defaults to `globalThis.fetch`). */\n readonly fetchImpl?: typeof fetch\n}\n\n/** Result of a {@link IngestionClient.flush}. */\nexport interface FlushResult {\n /** HTTP status, or `0` when the batch was empty and no request was made. */\n readonly status: number\n /** Number of batch items actually sent. */\n readonly sent: number\n /** Parsed JSON response body (or the raw text if it wasn't JSON), `null` on an empty flush. */\n readonly body: unknown\n}\n\n/**\n * One Langfuse ingestion batch item. `id` is the batch-envelope id — Langfuse's\n * idempotency/dedup key, which MUST be a unique string per operation (a create\n * and a later update of the same object share a body id but need distinct\n * envelope ids). `body` is any JSON object (a `trace-create`, `generation-create`,\n * `span-update`, `score-create`, … payload).\n */\nexport interface IngestionItem {\n readonly id: string\n readonly type: string\n readonly timestamp: string\n readonly body: object\n}\n\n/** A batching Langfuse ingestion transport. */\nexport interface IngestionClient {\n /** Buffer one item for the next flush. */\n enqueue(item: IngestionItem): void\n /** Number of buffered items not yet flushed. */\n size(): number\n /** Drain the buffer and POST it. Atomic: items enqueued during the in-flight\n * request stay buffered for the next flush rather than being dropped. */\n flush(): Promise<FlushResult>\n}\n\n/** Build a batching Langfuse ingestion client. */\nexport function createIngestionClient(cfg: IngestionConfig): IngestionClient {\n const fetchImpl = cfg.fetchImpl ?? globalThis.fetch\n const auth = `Basic ${Buffer.from(`${cfg.publicKey}:${cfg.secretKey}`).toString(\"base64\")}`\n const url = `${cfg.baseUrl}/api/public/ingestion`\n const batch: IngestionItem[] = []\n\n return {\n enqueue(item: IngestionItem): void {\n batch.push(item)\n },\n size(): number {\n return batch.length\n },\n async flush(): Promise<FlushResult> {\n // Take the current buffer up front so anything enqueued while the POST is\n // in flight is preserved for the next flush (a naive clear-after-await\n // would drop those). Empty batch → no request.\n const items = batch.splice(0, batch.length)\n if (items.length === 0) {\n return { status: 0, sent: 0, body: null }\n }\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: auth,\n },\n body: JSON.stringify({ batch: items }),\n })\n const text = await response.text()\n let body: unknown\n try {\n body = JSON.parse(text)\n } catch {\n body = text\n }\n return { status: response.status, sent: items.length, body }\n },\n }\n}\n","/**\n * Dependency-free Langfuse ingestion REST API sink.\n *\n * Maps the agentproto {@link EvalEvent} stream into Langfuse `trace-create`\n * and `score-create` objects and batches them for the public ingestion\n * endpoint (`POST /api/public/ingestion`). No Langfuse SDK is used.\n */\n\nimport type { EvalEvent } from \"@agentproto/eval\"\nimport type { Telemetry } from \"@agentproto/telemetry\"\nimport { createIngestionClient, type FlushResult } from \"./ingestion.js\"\n\nexport type { FlushResult } from \"./ingestion.js\"\n\n/** Configuration for the Langfuse telemetry sink. */\nexport interface LangfuseTelemetryConfig {\n /** Langfuse public key (used as the Basic auth username). */\n readonly publicKey: string\n /** Langfuse secret key (used as the Basic auth password). */\n readonly secretKey: string\n /** Langfuse base URL, e.g. `https://cloud.langfuse.com`. */\n readonly baseUrl: string\n /** Optional environment label forwarded to Langfuse objects. */\n readonly environment?: string\n /** Optional fetch implementation (defaults to `globalThis.fetch`). */\n readonly fetchImpl?: typeof fetch\n}\n\n/** Public type of the sink returned by {@link langfuseTelemetry}. */\nexport type LangfuseTelemetrySink = Telemetry<EvalEvent> & {\n flush(): Promise<FlushResult>\n}\n\n/** A JSON-serializable value — Langfuse `input`/`output`/`metadata` payloads. */\ntype JsonValue =\n | string\n | number\n | boolean\n | null\n | { readonly [key: string]: JsonValue }\n | readonly JsonValue[]\n\ntype TraceCreateBody = {\n readonly id: string\n readonly name: string\n readonly timestamp: string\n readonly environment?: string\n /** Suite descriptor, set on `eval.started`. */\n readonly input?: JsonValue\n /** Aggregate result, upserted on `eval.finished`. */\n readonly output?: JsonValue\n readonly metadata?: {\n readonly caseCount: number\n readonly scorerCount: number\n }\n}\n\ntype ScoreCreateBody = {\n readonly id: string\n readonly traceId: string\n readonly name: string\n readonly value: number\n readonly dataType: \"NUMERIC\"\n readonly comment?: string\n /** Nests per-case scores under their case span (see {@link SpanCreateBody}). */\n readonly observationId?: string\n readonly timestamp: string\n readonly environment?: string\n}\n\ntype SpanCreateBody = {\n readonly id: string\n readonly traceId: string\n readonly name: string\n readonly startTime: string\n readonly input?: JsonValue\n readonly environment?: string\n}\n\ntype SpanUpdateBody = {\n readonly id: string\n readonly traceId: string\n readonly endTime: string\n readonly output?: JsonValue\n readonly environment?: string\n}\n\n// Envelope ids are derived `${bodyId}#${operation}` (see the push helpers): a\n// unique string per operation so Langfuse's dedup doesn't collapse a create and\n// a later upsert/update of the same object. Items are transported by\n// `createIngestionClient` (see ./ingestion.ts).\n\n/**\n * Build a Langfuse ingestion sink for {@link EvalEvent}s.\n *\n * `emit()` buffers events into a batch. `flush()` POSTs the batch to\n * `${cfg.baseUrl}/api/public/ingestion` with Basic auth and returns the\n * HTTP status plus parsed response body.\n */\nexport function langfuseTelemetry(cfg: LangfuseTelemetryConfig): LangfuseTelemetrySink {\n const client = createIngestionClient(cfg)\n const tracedRuns = new Set<string>()\n\n /** Stable observation id for a case's span (scores nest under it). */\n const caseSpanId = (runId: string, caseId: string): string => `${runId}:case:${caseId}`\n\n /** Add the optional `environment` label to any body without an `as` cast. */\n function withEnv<T extends { readonly environment?: string }>(body: T): T {\n if (cfg.environment !== undefined) return { ...body, environment: cfg.environment }\n return body\n }\n\n function pushTrace(op: string, timestamp: string, body: TraceCreateBody): void {\n client.enqueue({ id: `${body.id}#${op}`, type: \"trace-create\", timestamp, body: withEnv(body) })\n }\n\n function pushScore(timestamp: string, body: ScoreCreateBody): void {\n client.enqueue({ id: `${body.id}#score`, type: \"score-create\", timestamp, body: withEnv(body) })\n }\n\n function pushSpanCreate(timestamp: string, body: SpanCreateBody): void {\n client.enqueue({ id: `${body.id}#span-create`, type: \"span-create\", timestamp, body: withEnv(body) })\n }\n\n function pushSpanUpdate(timestamp: string, body: SpanUpdateBody): void {\n client.enqueue({ id: `${body.id}#span-update`, type: \"span-update\", timestamp, body: withEnv(body) })\n }\n\n return {\n emit(event: EvalEvent): void {\n switch (event.kind) {\n case \"eval.started\": {\n if (tracedRuns.has(event.runId)) return\n tracedRuns.add(event.runId)\n pushTrace(\"create\", event.at, {\n id: event.runId,\n name: `eval:${event.suiteId}`,\n timestamp: event.at,\n input: {\n suiteId: event.suiteId,\n caseCount: event.caseCount,\n scorerCount: event.scorerCount,\n },\n metadata: {\n caseCount: event.caseCount,\n scorerCount: event.scorerCount,\n },\n })\n break\n }\n case \"eval.case.started\": {\n // Open a per-case span so the trace has a nested tree and each case's\n // scores hang under their own observation.\n pushSpanCreate(event.at, {\n id: caseSpanId(event.runId, event.caseId),\n traceId: event.runId,\n name: `case:${event.caseId}`,\n startTime: event.at,\n input: { caseId: event.caseId },\n })\n break\n }\n case \"eval.case.scored\": {\n pushScore(event.at, {\n id: `${event.runId}:${event.caseId}:${event.scorerId}`,\n traceId: event.runId,\n name: event.scorerId,\n value: event.value,\n dataType: \"NUMERIC\",\n comment: `case=${event.caseId} passed=${event.passed}`,\n observationId: caseSpanId(event.runId, event.caseId),\n timestamp: event.at,\n })\n break\n }\n case \"eval.case.finished\": {\n // Close the case span with its pass/fail outcome.\n pushSpanUpdate(event.at, {\n id: caseSpanId(event.runId, event.caseId),\n traceId: event.runId,\n endTime: event.at,\n output: { passed: event.passed },\n })\n break\n }\n case \"eval.finished\": {\n const passRate = event.total > 0 ? event.passedCount / event.total : 0\n // Upsert the trace with its aggregate outcome (same body id, distinct\n // envelope op so it is not deduped against the `create`).\n pushTrace(\"output\", event.at, {\n id: event.runId,\n name: `eval:${event.suiteId}`,\n timestamp: event.at,\n output: {\n total: event.total,\n passedCount: event.passedCount,\n failedCount: event.total - event.passedCount,\n meanValue: event.meanValue,\n passRate,\n durationMs: event.durationMs,\n },\n })\n for (const name of [\"eval.meanValue\", \"eval.passRate\"] as const) {\n const value = name === \"eval.meanValue\" ? event.meanValue : passRate\n pushScore(event.at, {\n id: `${event.runId}:${name}`,\n traceId: event.runId,\n name,\n value,\n dataType: \"NUMERIC\",\n timestamp: event.at,\n })\n }\n break\n }\n default: {\n // Forward-compat: ignore unknown event kinds.\n const _exhaustive: never = event\n void _exhaustive\n }\n }\n },\n\n flush(): Promise<FlushResult> {\n return client.flush()\n },\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentproto/telemetry-langfuse",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Langfuse ingestion REST API sink for @agentproto/telemetry. Dependency-free: maps EvalEvent batches to Langfuse trace-create and score-create objects over fetch.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agentproto",
|
|
7
|
+
"telemetry",
|
|
8
|
+
"langfuse",
|
|
9
|
+
"eval",
|
|
10
|
+
"observability",
|
|
11
|
+
"open-standard",
|
|
12
|
+
"agentic"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://agentproto.sh",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/agentproto/ts",
|
|
18
|
+
"directory": "packages/telemetry-langfuse"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/agentproto/ts/issues"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "dist/index.mjs",
|
|
26
|
+
"module": "dist/index.mjs",
|
|
27
|
+
"types": "dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.mjs",
|
|
32
|
+
"default": "./dist/index.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@agentproto/eval": "0.2.0",
|
|
46
|
+
"@agentproto/telemetry": "0.2.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^25.6.2",
|
|
50
|
+
"tsup": "^8.5.1",
|
|
51
|
+
"typescript": "^5.9.3",
|
|
52
|
+
"vitest": "^3.2.4",
|
|
53
|
+
"@agentproto/tooling": "0.1.0-alpha.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=20.9.0"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"dev": "tsup --watch",
|
|
60
|
+
"build": "tsup",
|
|
61
|
+
"clean": "rm -rf dist",
|
|
62
|
+
"check-types": "tsc --noEmit",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"test:watch": "vitest"
|
|
65
|
+
}
|
|
66
|
+
}
|