@struktur/sdk 2.0.0 → 2.1.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/package.json +2 -2
- package/src/auth/config.ts +57 -0
- package/src/extract.ts +55 -19
- package/src/index.ts +13 -0
- package/src/llm/LLMClient.ts +88 -7
- package/src/llm/RetryingRunner.ts +83 -1
- package/src/strategies/DoublePassAutoMergeStrategy.ts +140 -0
- package/src/strategies/DoublePassStrategy.ts +87 -0
- package/src/strategies/ParallelAutoMergeStrategy.ts +104 -0
- package/src/strategies/ParallelStrategy.ts +51 -0
- package/src/strategies/SequentialAutoMergeStrategy.ts +103 -0
- package/src/strategies/SequentialStrategy.ts +23 -0
- package/src/strategies/SimpleStrategy.ts +20 -0
- package/src/strategies/utils.ts +42 -3
- package/src/types.ts +66 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@struktur/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"license": "FSL-1.1-MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"@ai-sdk/google": "^3.0.0",
|
|
21
21
|
"@ai-sdk/openai": "^3.0.0",
|
|
22
22
|
"@openrouter/ai-sdk-provider": "^2.0.0",
|
|
23
|
-
"@struktur/agent-strategy": "1.0
|
|
23
|
+
"@struktur/agent-strategy": "2.1.0",
|
|
24
24
|
"ai": "^6.0.97",
|
|
25
25
|
"ajv": "^8.17.1",
|
|
26
26
|
"ajv-formats": "^3.0.1",
|
package/src/auth/config.ts
CHANGED
|
@@ -3,11 +3,24 @@ import os from "node:os";
|
|
|
3
3
|
import { chmod, mkdir } from "node:fs/promises";
|
|
4
4
|
import type { ParserDef, ParsersConfig } from "@struktur/sdk";
|
|
5
5
|
|
|
6
|
+
type TelemetryConfig = {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
provider: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
projectName?: string;
|
|
12
|
+
publicKey?: string; // For Langfuse
|
|
13
|
+
secretKey?: string; // For Langfuse
|
|
14
|
+
baseUrl?: string; // For Langfuse
|
|
15
|
+
sampleRate?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
6
18
|
type ConfigStore = {
|
|
7
19
|
version: 1;
|
|
8
20
|
defaultModel?: string;
|
|
9
21
|
aliases?: Record<string, string>;
|
|
10
22
|
parsers?: ParsersConfig;
|
|
23
|
+
telemetry?: TelemetryConfig;
|
|
11
24
|
};
|
|
12
25
|
|
|
13
26
|
const CONFIG_DIR_ENV = "STRUKTUR_CONFIG_DIR";
|
|
@@ -127,3 +140,47 @@ export const deleteParser = async (mimeType: string): Promise<boolean> => {
|
|
|
127
140
|
await writeConfigStore(store);
|
|
128
141
|
return true;
|
|
129
142
|
};
|
|
143
|
+
|
|
144
|
+
// --- Telemetry config management ---
|
|
145
|
+
|
|
146
|
+
export const getTelemetryConfig = async (): Promise<TelemetryConfig | undefined> => {
|
|
147
|
+
const store = await readConfigStore();
|
|
148
|
+
return store.telemetry;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const setTelemetryConfig = async (config: TelemetryConfig): Promise<void> => {
|
|
152
|
+
const store = await readConfigStore();
|
|
153
|
+
store.telemetry = config;
|
|
154
|
+
await writeConfigStore(store);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const enableTelemetry = async (
|
|
158
|
+
provider: string,
|
|
159
|
+
options: Omit<TelemetryConfig, "enabled" | "provider">
|
|
160
|
+
): Promise<void> => {
|
|
161
|
+
const store = await readConfigStore();
|
|
162
|
+
store.telemetry = {
|
|
163
|
+
enabled: true,
|
|
164
|
+
provider,
|
|
165
|
+
...options,
|
|
166
|
+
};
|
|
167
|
+
await writeConfigStore(store);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const disableTelemetry = async (): Promise<void> => {
|
|
171
|
+
const store = await readConfigStore();
|
|
172
|
+
if (store.telemetry) {
|
|
173
|
+
store.telemetry.enabled = false;
|
|
174
|
+
}
|
|
175
|
+
await writeConfigStore(store);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const deleteTelemetryConfig = async (): Promise<boolean> => {
|
|
179
|
+
const store = await readConfigStore();
|
|
180
|
+
if (!store.telemetry) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
delete store.telemetry;
|
|
184
|
+
await writeConfigStore(store);
|
|
185
|
+
return true;
|
|
186
|
+
};
|
package/src/extract.ts
CHANGED
|
@@ -34,29 +34,52 @@ export const extract = async <T>(
|
|
|
34
34
|
options: ExtractionOptions<T>,
|
|
35
35
|
): Promise<ExtractionResult<T>> => {
|
|
36
36
|
const debug = options.debug;
|
|
37
|
+
const telemetry = options.telemetry;
|
|
37
38
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
const schema = resolveSchema(options);
|
|
43
|
-
resolvedOptions = { ...options, schema };
|
|
44
|
-
} catch (error) {
|
|
45
|
-
debug?.extractionComplete({
|
|
46
|
-
success: false,
|
|
47
|
-
totalInputTokens: 0,
|
|
48
|
-
totalOutputTokens: 0,
|
|
49
|
-
totalTokens: 0,
|
|
50
|
-
error: (error as Error).message,
|
|
51
|
-
});
|
|
52
|
-
return {
|
|
53
|
-
data: null as unknown as T,
|
|
54
|
-
usage: emptyUsage,
|
|
55
|
-
error: error as Error,
|
|
56
|
-
};
|
|
39
|
+
// Initialize telemetry if provided
|
|
40
|
+
if (telemetry) {
|
|
41
|
+
await telemetry.initialize();
|
|
57
42
|
}
|
|
58
43
|
|
|
44
|
+
// Start root extraction span
|
|
45
|
+
const rootSpan = telemetry?.startSpan({
|
|
46
|
+
name: "struktur.extract",
|
|
47
|
+
kind: "CHAIN",
|
|
48
|
+
attributes: {
|
|
49
|
+
"extraction.strategy": options.strategy?.name ?? "default",
|
|
50
|
+
"extraction.artifacts.count": options.artifacts.length,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
59
54
|
try {
|
|
55
|
+
// Validate mutual exclusion and resolve the concrete schema early so that
|
|
56
|
+
// every strategy receives a fully-populated options object.
|
|
57
|
+
let resolvedOptions: ExtractionOptions<T>;
|
|
58
|
+
try {
|
|
59
|
+
const schema = resolveSchema(options);
|
|
60
|
+
resolvedOptions = { ...options, schema };
|
|
61
|
+
} catch (error) {
|
|
62
|
+
debug?.extractionComplete({
|
|
63
|
+
success: false,
|
|
64
|
+
totalInputTokens: 0,
|
|
65
|
+
totalOutputTokens: 0,
|
|
66
|
+
totalTokens: 0,
|
|
67
|
+
error: (error as Error).message,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
telemetry?.endSpan(rootSpan!, {
|
|
71
|
+
status: "error",
|
|
72
|
+
error: error as Error,
|
|
73
|
+
});
|
|
74
|
+
await telemetry?.shutdown();
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
data: null as unknown as T,
|
|
78
|
+
usage: emptyUsage,
|
|
79
|
+
error: error as Error,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
const total = resolvedOptions.strategy.getEstimatedSteps?.(resolvedOptions.artifacts);
|
|
61
84
|
|
|
62
85
|
debug?.strategyRunStart({
|
|
@@ -95,6 +118,13 @@ export const extract = async <T>(
|
|
|
95
118
|
error: result.error?.message,
|
|
96
119
|
});
|
|
97
120
|
|
|
121
|
+
telemetry?.endSpan(rootSpan!, {
|
|
122
|
+
status: result.error ? "error" : "ok",
|
|
123
|
+
output: result.data,
|
|
124
|
+
error: result.error,
|
|
125
|
+
});
|
|
126
|
+
await telemetry?.shutdown();
|
|
127
|
+
|
|
98
128
|
return result;
|
|
99
129
|
} catch (error) {
|
|
100
130
|
debug?.extractionComplete({
|
|
@@ -105,6 +135,12 @@ export const extract = async <T>(
|
|
|
105
135
|
error: (error as Error).message,
|
|
106
136
|
});
|
|
107
137
|
|
|
138
|
+
telemetry?.endSpan(rootSpan!, {
|
|
139
|
+
status: "error",
|
|
140
|
+
error: error as Error,
|
|
141
|
+
});
|
|
142
|
+
await telemetry?.shutdown();
|
|
143
|
+
|
|
108
144
|
return {
|
|
109
145
|
data: null as unknown as T,
|
|
110
146
|
usage: emptyUsage,
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,14 @@ export type {
|
|
|
10
10
|
Usage,
|
|
11
11
|
AnyJSONSchema,
|
|
12
12
|
TypedJSONSchema,
|
|
13
|
+
// Agent event types
|
|
14
|
+
AgentEvents,
|
|
15
|
+
AgentToolStartInfo,
|
|
16
|
+
AgentToolEndInfo,
|
|
17
|
+
AgentMessageInfo,
|
|
18
|
+
AgentReasoningInfo,
|
|
19
|
+
// Telemetry
|
|
20
|
+
TelemetryAdapter,
|
|
13
21
|
} from "./types";
|
|
14
22
|
|
|
15
23
|
export { extract } from "./extract";
|
|
@@ -83,6 +91,11 @@ export {
|
|
|
83
91
|
getParser,
|
|
84
92
|
setParser,
|
|
85
93
|
deleteParser,
|
|
94
|
+
getTelemetryConfig,
|
|
95
|
+
setTelemetryConfig,
|
|
96
|
+
enableTelemetry,
|
|
97
|
+
disableTelemetry,
|
|
98
|
+
deleteTelemetryConfig,
|
|
86
99
|
} from "./auth/config";
|
|
87
100
|
export {
|
|
88
101
|
listStoredProviders,
|
package/src/llm/LLMClient.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generateText, Output, jsonSchema, type ModelMessage } from "ai";
|
|
2
|
-
import type { AnyJSONSchema, Usage } from "../types";
|
|
2
|
+
import type { AnyJSONSchema, Usage, TelemetryAdapter } from "../types";
|
|
3
3
|
import type { UserContent } from "./message";
|
|
4
4
|
|
|
5
5
|
type GenerateTextParams = Parameters<typeof generateText>[0];
|
|
@@ -15,6 +15,14 @@ export type StructuredRequest<T> = {
|
|
|
15
15
|
schemaName?: string;
|
|
16
16
|
schemaDescription?: string;
|
|
17
17
|
strict?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Telemetry adapter for tracing LLM calls
|
|
20
|
+
*/
|
|
21
|
+
telemetry?: TelemetryAdapter;
|
|
22
|
+
/**
|
|
23
|
+
* Parent span for creating hierarchical traces
|
|
24
|
+
*/
|
|
25
|
+
parentSpan?: { id: string; traceId: string; name: string; kind: string; startTime: number; parentId?: string };
|
|
18
26
|
};
|
|
19
27
|
|
|
20
28
|
export type StructuredResponse<T> = {
|
|
@@ -36,6 +44,21 @@ const isZodSchema = (
|
|
|
36
44
|
export const generateStructured = async <T>(
|
|
37
45
|
request: StructuredRequest<T>,
|
|
38
46
|
): Promise<StructuredResponse<T>> => {
|
|
47
|
+
const { telemetry, parentSpan } = request;
|
|
48
|
+
|
|
49
|
+
// Start LLM span if telemetry is enabled
|
|
50
|
+
const llmSpan = telemetry?.startSpan({
|
|
51
|
+
name: "llm.generateStructured",
|
|
52
|
+
kind: "LLM",
|
|
53
|
+
parentSpan,
|
|
54
|
+
attributes: {
|
|
55
|
+
"llm.schema_name": request.schemaName ?? "extract",
|
|
56
|
+
"llm.strict": request.strict ?? false,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const startTime = Date.now();
|
|
61
|
+
|
|
39
62
|
const schema = isZodSchema(request.schema)
|
|
40
63
|
? request.schema
|
|
41
64
|
: jsonSchema(request.schema as AnyJSONSchema);
|
|
@@ -84,6 +107,13 @@ export const generateStructured = async <T>(
|
|
|
84
107
|
...(providerOptions ? { providerOptions } : {}),
|
|
85
108
|
});
|
|
86
109
|
} catch (error) {
|
|
110
|
+
// Determine model ID for error messages
|
|
111
|
+
const modelId =
|
|
112
|
+
typeof request.model === "object" && request.model !== null
|
|
113
|
+
? (request.model as { modelId?: string }).modelId ??
|
|
114
|
+
JSON.stringify(request.model)
|
|
115
|
+
: String(request.model);
|
|
116
|
+
|
|
87
117
|
if (
|
|
88
118
|
error &&
|
|
89
119
|
typeof error === "object" &&
|
|
@@ -101,12 +131,6 @@ export const generateStructured = async <T>(
|
|
|
101
131
|
};
|
|
102
132
|
};
|
|
103
133
|
|
|
104
|
-
const modelId =
|
|
105
|
-
typeof request.model === "object" && request.model !== null
|
|
106
|
-
? (request.model as { modelId?: string }).modelId ??
|
|
107
|
-
JSON.stringify(request.model)
|
|
108
|
-
: String(request.model);
|
|
109
|
-
|
|
110
134
|
const responseBody = apiError.responseBody;
|
|
111
135
|
const errorData = apiError.data;
|
|
112
136
|
|
|
@@ -156,6 +180,30 @@ export const generateStructured = async <T>(
|
|
|
156
180
|
);
|
|
157
181
|
}
|
|
158
182
|
}
|
|
183
|
+
|
|
184
|
+
// Record error in telemetry
|
|
185
|
+
if (llmSpan && telemetry) {
|
|
186
|
+
const latencyMs = Date.now() - startTime;
|
|
187
|
+
telemetry.recordEvent(llmSpan, {
|
|
188
|
+
type: "llm_call",
|
|
189
|
+
model: modelId,
|
|
190
|
+
provider: "unknown", // Will be determined by the model
|
|
191
|
+
input: {
|
|
192
|
+
messages: request.messages ?? [{ role: "user", content: typeof request.user === "string" ? request.user : "" }],
|
|
193
|
+
temperature: undefined,
|
|
194
|
+
maxTokens: undefined,
|
|
195
|
+
schema: request.schema,
|
|
196
|
+
},
|
|
197
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
198
|
+
latencyMs,
|
|
199
|
+
});
|
|
200
|
+
telemetry.endSpan(llmSpan, {
|
|
201
|
+
status: "error",
|
|
202
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
203
|
+
latencyMs,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
159
207
|
throw error;
|
|
160
208
|
}
|
|
161
209
|
|
|
@@ -179,5 +227,38 @@ export const generateStructured = async <T>(
|
|
|
179
227
|
totalTokens,
|
|
180
228
|
};
|
|
181
229
|
|
|
230
|
+
// Record successful LLM call in telemetry
|
|
231
|
+
if (llmSpan && telemetry) {
|
|
232
|
+
const latencyMs = Date.now() - startTime;
|
|
233
|
+
telemetry.recordEvent(llmSpan, {
|
|
234
|
+
type: "llm_call",
|
|
235
|
+
model: typeof request.model === "object" && request.model !== null
|
|
236
|
+
? (request.model as { modelId?: string }).modelId ?? "unknown"
|
|
237
|
+
: String(request.model),
|
|
238
|
+
provider: preferredProvider ?? "unknown",
|
|
239
|
+
input: {
|
|
240
|
+
messages: request.messages ?? [{ role: "user", content: typeof request.user === "string" ? request.user : "" }],
|
|
241
|
+
temperature: undefined,
|
|
242
|
+
maxTokens: undefined,
|
|
243
|
+
schema: request.schema,
|
|
244
|
+
},
|
|
245
|
+
output: {
|
|
246
|
+
content: JSON.stringify(result.output),
|
|
247
|
+
structured: true,
|
|
248
|
+
usage: {
|
|
249
|
+
input: inputTokens,
|
|
250
|
+
output: outputTokens,
|
|
251
|
+
total: totalTokens,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
latencyMs,
|
|
255
|
+
});
|
|
256
|
+
telemetry.endSpan(llmSpan, {
|
|
257
|
+
status: "ok",
|
|
258
|
+
output: result.output,
|
|
259
|
+
latencyMs,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
182
263
|
return { data: result.output as T, usage };
|
|
183
264
|
};
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
validateAllowingMissingRequired,
|
|
6
6
|
} from "../validation/validator";
|
|
7
7
|
import type { ModelMessage } from "ai";
|
|
8
|
-
import type { ExtractionEvents, Usage } from "../types";
|
|
8
|
+
import type { ExtractionEvents, Usage, TelemetryAdapter } from "../types";
|
|
9
9
|
import type { DebugLogger } from "../debug/logger";
|
|
10
10
|
import { generateStructured } from "./LLMClient";
|
|
11
11
|
import type { UserContent } from "./message";
|
|
@@ -22,9 +22,30 @@ export type RetryOptions<T> = {
|
|
|
22
22
|
strict?: boolean;
|
|
23
23
|
debug?: DebugLogger;
|
|
24
24
|
callId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Telemetry adapter for tracing validation and retries
|
|
27
|
+
*/
|
|
28
|
+
telemetry?: TelemetryAdapter;
|
|
29
|
+
/**
|
|
30
|
+
* Parent span for creating hierarchical traces
|
|
31
|
+
*/
|
|
32
|
+
parentSpan?: { id: string; traceId: string; name: string; kind: string; startTime: number; parentId?: string };
|
|
25
33
|
};
|
|
26
34
|
|
|
27
35
|
export const runWithRetries = async <T>(options: RetryOptions<T>) => {
|
|
36
|
+
const { telemetry, parentSpan } = options;
|
|
37
|
+
|
|
38
|
+
// Start validation/retry span if telemetry is enabled
|
|
39
|
+
const retrySpan = telemetry?.startSpan({
|
|
40
|
+
name: "struktur.validation_retry",
|
|
41
|
+
kind: "CHAIN",
|
|
42
|
+
parentSpan,
|
|
43
|
+
attributes: {
|
|
44
|
+
"retry.max_attempts": options.maxAttempts ?? 3,
|
|
45
|
+
"retry.schema_name": options.schemaName ?? "extract",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
28
49
|
const ajv = createAjv();
|
|
29
50
|
const maxAttempts = options.maxAttempts ?? 3;
|
|
30
51
|
const messages: ModelMessage[] = [{ role: "user", content: options.user }];
|
|
@@ -76,6 +97,8 @@ export const runWithRetries = async <T>(options: RetryOptions<T>) => {
|
|
|
76
97
|
user: options.user,
|
|
77
98
|
messages,
|
|
78
99
|
strict: options.strict,
|
|
100
|
+
telemetry,
|
|
101
|
+
parentSpan: retrySpan,
|
|
79
102
|
});
|
|
80
103
|
const durationMs = Date.now() - startTime;
|
|
81
104
|
|
|
@@ -105,6 +128,24 @@ export const runWithRetries = async <T>(options: RetryOptions<T>) => {
|
|
|
105
128
|
durationMs,
|
|
106
129
|
});
|
|
107
130
|
|
|
131
|
+
// Record successful validation
|
|
132
|
+
if (retrySpan && telemetry) {
|
|
133
|
+
telemetry.recordEvent(retrySpan, {
|
|
134
|
+
type: "validation",
|
|
135
|
+
attempt,
|
|
136
|
+
maxAttempts,
|
|
137
|
+
schema: options.schema,
|
|
138
|
+
input: result.data,
|
|
139
|
+
success: true,
|
|
140
|
+
latencyMs: durationMs,
|
|
141
|
+
});
|
|
142
|
+
telemetry.endSpan(retrySpan, {
|
|
143
|
+
status: "ok",
|
|
144
|
+
output: validated,
|
|
145
|
+
latencyMs: durationMs,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
108
149
|
return { data: validated, usage };
|
|
109
150
|
} else {
|
|
110
151
|
const validationResult = validateAllowingMissingRequired<T>(
|
|
@@ -125,6 +166,24 @@ export const runWithRetries = async <T>(options: RetryOptions<T>) => {
|
|
|
125
166
|
durationMs,
|
|
126
167
|
});
|
|
127
168
|
|
|
169
|
+
// Record successful validation
|
|
170
|
+
if (retrySpan && telemetry) {
|
|
171
|
+
telemetry.recordEvent(retrySpan, {
|
|
172
|
+
type: "validation",
|
|
173
|
+
attempt,
|
|
174
|
+
maxAttempts,
|
|
175
|
+
schema: options.schema,
|
|
176
|
+
input: result.data,
|
|
177
|
+
success: true,
|
|
178
|
+
latencyMs: durationMs,
|
|
179
|
+
});
|
|
180
|
+
telemetry.endSpan(retrySpan, {
|
|
181
|
+
status: "ok",
|
|
182
|
+
output: validationResult.data,
|
|
183
|
+
latencyMs: durationMs,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
128
187
|
return { data: validationResult.data, usage };
|
|
129
188
|
}
|
|
130
189
|
|
|
@@ -143,6 +202,20 @@ export const runWithRetries = async <T>(options: RetryOptions<T>) => {
|
|
|
143
202
|
errors: error.errors,
|
|
144
203
|
});
|
|
145
204
|
|
|
205
|
+
// Record failed validation
|
|
206
|
+
if (retrySpan && telemetry) {
|
|
207
|
+
telemetry.recordEvent(retrySpan, {
|
|
208
|
+
type: "validation",
|
|
209
|
+
attempt,
|
|
210
|
+
maxAttempts,
|
|
211
|
+
schema: options.schema,
|
|
212
|
+
input: result.data,
|
|
213
|
+
success: false,
|
|
214
|
+
errors: error.errors,
|
|
215
|
+
latencyMs: durationMs,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
146
219
|
// Emit retry event before attempting retry
|
|
147
220
|
const nextAttempt = attempt + 1;
|
|
148
221
|
if (nextAttempt <= maxAttempts) {
|
|
@@ -180,6 +253,15 @@ export const runWithRetries = async <T>(options: RetryOptions<T>) => {
|
|
|
180
253
|
error: (error as Error).message,
|
|
181
254
|
});
|
|
182
255
|
|
|
256
|
+
// Record error in telemetry
|
|
257
|
+
if (retrySpan && telemetry) {
|
|
258
|
+
telemetry.endSpan(retrySpan, {
|
|
259
|
+
status: "error",
|
|
260
|
+
error: error as Error,
|
|
261
|
+
latencyMs: durationMs,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
183
265
|
break;
|
|
184
266
|
}
|
|
185
267
|
}
|
|
@@ -84,6 +84,20 @@ export class DoublePassAutoMergeStrategy<T> implements ExtractionStrategy<T> {
|
|
|
84
84
|
|
|
85
85
|
async run(options: ExtractionOptions<T>): Promise<ExtractionResult<T>> {
|
|
86
86
|
const debug = options.debug;
|
|
87
|
+
const { telemetry } = options;
|
|
88
|
+
|
|
89
|
+
// Create strategy-level span
|
|
90
|
+
const strategySpan = telemetry?.startSpan({
|
|
91
|
+
name: "strategy.double-pass-auto-merge",
|
|
92
|
+
kind: "CHAIN",
|
|
93
|
+
attributes: {
|
|
94
|
+
"strategy.name": this.name,
|
|
95
|
+
"strategy.artifacts.count": options.artifacts.length,
|
|
96
|
+
"strategy.chunk_size": this.config.chunkSize,
|
|
97
|
+
"strategy.concurrency": this.config.concurrency,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
87
101
|
const batches = getBatches(
|
|
88
102
|
options.artifacts,
|
|
89
103
|
{
|
|
@@ -91,11 +105,24 @@ export class DoublePassAutoMergeStrategy<T> implements ExtractionStrategy<T> {
|
|
|
91
105
|
maxImages: this.config.maxImages,
|
|
92
106
|
},
|
|
93
107
|
debug,
|
|
108
|
+
telemetry ?? undefined,
|
|
109
|
+
strategySpan,
|
|
94
110
|
);
|
|
95
111
|
|
|
96
112
|
const schema = serializeSchema(options.schema);
|
|
97
113
|
const totalSteps = this.getEstimatedSteps(options.artifacts);
|
|
98
114
|
let step = 1;
|
|
115
|
+
|
|
116
|
+
// Create pass 1 span
|
|
117
|
+
const pass1Span = telemetry?.startSpan({
|
|
118
|
+
name: "struktur.pass_1",
|
|
119
|
+
kind: "CHAIN",
|
|
120
|
+
parentSpan: strategySpan,
|
|
121
|
+
attributes: {
|
|
122
|
+
"pass.number": 1,
|
|
123
|
+
"pass.type": "parallel_extraction",
|
|
124
|
+
},
|
|
125
|
+
});
|
|
99
126
|
|
|
100
127
|
const tasks = batches.map((batch, index) => async () => {
|
|
101
128
|
const prompt = buildExtractorPrompt(
|
|
@@ -114,6 +141,8 @@ export class DoublePassAutoMergeStrategy<T> implements ExtractionStrategy<T> {
|
|
|
114
141
|
strict: options.strict ?? this.config.strict,
|
|
115
142
|
debug,
|
|
116
143
|
callId: `double_pass_auto_1_batch_${index + 1}`,
|
|
144
|
+
telemetry: telemetry ?? undefined,
|
|
145
|
+
parentSpan: pass1Span,
|
|
117
146
|
});
|
|
118
147
|
step += 1;
|
|
119
148
|
await options.events?.onStep?.({
|
|
@@ -145,6 +174,17 @@ export class DoublePassAutoMergeStrategy<T> implements ExtractionStrategy<T> {
|
|
|
145
174
|
inputCount: results.length,
|
|
146
175
|
strategy: this.name,
|
|
147
176
|
});
|
|
177
|
+
|
|
178
|
+
// Create smart merge span
|
|
179
|
+
const mergeSpan = telemetry?.startSpan({
|
|
180
|
+
name: "struktur.smart_merge",
|
|
181
|
+
kind: "CHAIN",
|
|
182
|
+
parentSpan: pass1Span,
|
|
183
|
+
attributes: {
|
|
184
|
+
"merge.strategy": "smart",
|
|
185
|
+
"merge.input_count": results.length,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
148
188
|
|
|
149
189
|
for (let i = 0; i < results.length; i++) {
|
|
150
190
|
const result = results[i]!;
|
|
@@ -168,12 +208,54 @@ export class DoublePassAutoMergeStrategy<T> implements ExtractionStrategy<T> {
|
|
|
168
208
|
leftCount: leftArray,
|
|
169
209
|
rightCount: rightArray,
|
|
170
210
|
});
|
|
211
|
+
|
|
212
|
+
// Record merge event in telemetry
|
|
213
|
+
if (mergeSpan && telemetry) {
|
|
214
|
+
telemetry.recordEvent(mergeSpan, {
|
|
215
|
+
type: "merge",
|
|
216
|
+
strategy: "smart",
|
|
217
|
+
inputCount: rightArray ?? 1,
|
|
218
|
+
outputCount: leftArray ?? 1,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
171
221
|
}
|
|
172
222
|
}
|
|
173
223
|
|
|
174
224
|
debug?.mergeComplete({ mergeId: "double_pass_auto_merge", success: true });
|
|
225
|
+
|
|
226
|
+
// End merge span
|
|
227
|
+
if (mergeSpan && telemetry) {
|
|
228
|
+
telemetry.endSpan(mergeSpan, {
|
|
229
|
+
status: "ok",
|
|
230
|
+
output: merged,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
175
233
|
|
|
176
234
|
merged = dedupeArrays(merged);
|
|
235
|
+
|
|
236
|
+
// Create exact dedupe span
|
|
237
|
+
const exactDedupeSpan = telemetry?.startSpan({
|
|
238
|
+
name: "struktur.exact_dedupe",
|
|
239
|
+
kind: "CHAIN",
|
|
240
|
+
parentSpan: pass1Span,
|
|
241
|
+
attributes: {
|
|
242
|
+
"dedupe.method": "exact_hashing",
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// End exact dedupe span
|
|
247
|
+
if (exactDedupeSpan && telemetry) {
|
|
248
|
+
telemetry.recordEvent(exactDedupeSpan, {
|
|
249
|
+
type: "merge",
|
|
250
|
+
strategy: "exact_hash_dedupe",
|
|
251
|
+
inputCount: Object.keys(merged).length,
|
|
252
|
+
outputCount: Object.keys(merged).length,
|
|
253
|
+
});
|
|
254
|
+
telemetry.endSpan(exactDedupeSpan, {
|
|
255
|
+
status: "ok",
|
|
256
|
+
output: merged,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
177
259
|
|
|
178
260
|
const dedupePrompt = buildDeduplicationPrompt(schema, merged);
|
|
179
261
|
|
|
@@ -181,6 +263,16 @@ export class DoublePassAutoMergeStrategy<T> implements ExtractionStrategy<T> {
|
|
|
181
263
|
dedupeId: "double_pass_auto_dedupe",
|
|
182
264
|
itemCount: Object.keys(merged).length,
|
|
183
265
|
});
|
|
266
|
+
|
|
267
|
+
// Create LLM dedupe span
|
|
268
|
+
const llmDedupeSpan = telemetry?.startSpan({
|
|
269
|
+
name: "struktur.llm_dedupe",
|
|
270
|
+
kind: "CHAIN",
|
|
271
|
+
parentSpan: pass1Span,
|
|
272
|
+
attributes: {
|
|
273
|
+
"dedupe.method": "llm",
|
|
274
|
+
},
|
|
275
|
+
});
|
|
184
276
|
|
|
185
277
|
const dedupeResponse = await runWithRetries<{ keys: string[] }>({
|
|
186
278
|
model: this.config.dedupeModel ?? this.config.model,
|
|
@@ -192,6 +284,8 @@ export class DoublePassAutoMergeStrategy<T> implements ExtractionStrategy<T> {
|
|
|
192
284
|
strict: this.config.strict,
|
|
193
285
|
debug,
|
|
194
286
|
callId: "double_pass_auto_dedupe",
|
|
287
|
+
telemetry: telemetry ?? undefined,
|
|
288
|
+
parentSpan: llmDedupeSpan,
|
|
195
289
|
});
|
|
196
290
|
|
|
197
291
|
step += 1;
|
|
@@ -217,9 +311,41 @@ export class DoublePassAutoMergeStrategy<T> implements ExtractionStrategy<T> {
|
|
|
217
311
|
duplicatesFound: dedupeResponse.data.keys.length,
|
|
218
312
|
itemsRemoved: dedupeResponse.data.keys.length,
|
|
219
313
|
});
|
|
314
|
+
|
|
315
|
+
// End LLM dedupe span
|
|
316
|
+
if (llmDedupeSpan && telemetry) {
|
|
317
|
+
telemetry.recordEvent(llmDedupeSpan, {
|
|
318
|
+
type: "merge",
|
|
319
|
+
strategy: "llm_dedupe",
|
|
320
|
+
inputCount: Object.keys(merged).length,
|
|
321
|
+
outputCount: Object.keys(deduped).length,
|
|
322
|
+
deduped: dedupeResponse.data.keys.length,
|
|
323
|
+
});
|
|
324
|
+
telemetry.endSpan(llmDedupeSpan, {
|
|
325
|
+
status: "ok",
|
|
326
|
+
output: deduped,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// End pass 1 span
|
|
331
|
+
telemetry?.endSpan(pass1Span!, {
|
|
332
|
+
status: "ok",
|
|
333
|
+
output: deduped,
|
|
334
|
+
});
|
|
220
335
|
|
|
221
336
|
let currentData = deduped as T;
|
|
222
337
|
const usages = [...results.map((r) => r.usage), dedupeResponse.usage];
|
|
338
|
+
|
|
339
|
+
// Create pass 2 span
|
|
340
|
+
const pass2Span = telemetry?.startSpan({
|
|
341
|
+
name: "struktur.pass_2",
|
|
342
|
+
kind: "CHAIN",
|
|
343
|
+
parentSpan: strategySpan,
|
|
344
|
+
attributes: {
|
|
345
|
+
"pass.number": 2,
|
|
346
|
+
"pass.type": "sequential_refinement",
|
|
347
|
+
},
|
|
348
|
+
});
|
|
223
349
|
|
|
224
350
|
for (const [index, batch] of batches.entries()) {
|
|
225
351
|
const prompt = buildSequentialPrompt(
|
|
@@ -240,6 +366,8 @@ export class DoublePassAutoMergeStrategy<T> implements ExtractionStrategy<T> {
|
|
|
240
366
|
strict: this.config.strict,
|
|
241
367
|
debug,
|
|
242
368
|
callId: `double_pass_auto_2_batch_${index + 1}`,
|
|
369
|
+
telemetry: telemetry ?? undefined,
|
|
370
|
+
parentSpan: pass2Span,
|
|
243
371
|
});
|
|
244
372
|
|
|
245
373
|
currentData = result.data;
|
|
@@ -258,6 +386,18 @@ export class DoublePassAutoMergeStrategy<T> implements ExtractionStrategy<T> {
|
|
|
258
386
|
strategy: this.name,
|
|
259
387
|
});
|
|
260
388
|
}
|
|
389
|
+
|
|
390
|
+
// End pass 2 span
|
|
391
|
+
telemetry?.endSpan(pass2Span!, {
|
|
392
|
+
status: "ok",
|
|
393
|
+
output: currentData,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// End strategy span
|
|
397
|
+
telemetry?.endSpan(strategySpan!, {
|
|
398
|
+
status: "ok",
|
|
399
|
+
output: currentData,
|
|
400
|
+
});
|
|
261
401
|
|
|
262
402
|
return { data: currentData, usage: mergeUsage(usages) };
|
|
263
403
|
}
|