@zhushanwen/pi-structured-output 0.2.2 → 0.3.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 +1 -1
- package/src/index.ts +66 -127
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structured Output Extension
|
|
2
|
+
* Structured Output Extension — 全局可用 tool
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* via turn_end + sendUserMessage.
|
|
4
|
+
* 始终注册 `structured-output` tool,AI 可在任何场景下调用。
|
|
5
|
+
* 调用时传入 schema + data,扩展用 Ajv 验证 data 是否符合 schema。
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Key design decisions (borrowed from Claude Code):
|
|
12
|
-
* - Schema is injected into BOTH system prompt AND tool description, so LLM
|
|
13
|
-
* knows the exact output structure regardless of which signal it reads.
|
|
14
|
-
* - Enforcement checks "last call succeeded" (not "was called"), so validation
|
|
15
|
-
* failures trigger retries rather than being silently skipped.
|
|
16
|
-
* - Retry cap prevents infinite enforcement loops on persistent schema mismatch.
|
|
17
|
-
* - WeakMap caches Ajv compile results for repeated calls with the same schema
|
|
18
|
-
* object reference (mirrors Claude Code's toolCache pattern).
|
|
7
|
+
* workflow 场景:agent-pool 通过 prompt 指示 AI 调用此 tool 并传入 schema。
|
|
8
|
+
* 普通对话场景:AI 需要返回结构化数据时自行调用。
|
|
19
9
|
*/
|
|
20
10
|
|
|
21
11
|
import Ajv, { type ValidateFunction } from "ajv";
|
|
@@ -25,127 +15,76 @@ import { Type } from "@sinclair/typebox";
|
|
|
25
15
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
16
|
type PiAPI = any;
|
|
27
17
|
|
|
28
|
-
const ENV_KEY = "STRUCTURED_OUTPUT_SCHEMA";
|
|
29
18
|
const TOOL_NAME = "structured-output";
|
|
30
|
-
const MAX_RETRIES = parseInt(process.env.MAX_STRUCTURED_OUTPUT_RETRIES || "5", 10);
|
|
31
|
-
|
|
32
|
-
const ENFORCEMENT_MESSAGE = "你必须调用 structured-output tool 来返回结果。";
|
|
33
19
|
|
|
34
20
|
// ── Ajv WeakMap cache ─────────────────────────────────────────
|
|
35
|
-
//
|
|
36
|
-
// Without caching, each call does new Ajv() + validateSchema() + compile().
|
|
37
|
-
// WeakMap keyed by schema object reference brings this to near-zero overhead.
|
|
21
|
+
// Repeated calls with the same schema object reference are cached.
|
|
38
22
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
23
|
const ajvCache = new WeakMap<object, ValidateFunction>();
|
|
40
24
|
|
|
41
25
|
function getOrCompileValidator(schema: Record<string, unknown>): ValidateFunction {
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
const cached = ajvCache.get(schema);
|
|
27
|
+
if (cached) return cached;
|
|
44
28
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
29
|
+
const ajv = new Ajv({ strict: false });
|
|
30
|
+
const validate = ajv.compile(schema);
|
|
31
|
+
ajvCache.set(schema, validate);
|
|
32
|
+
return validate;
|
|
49
33
|
}
|
|
50
34
|
|
|
51
35
|
export default function structuredOutputExtension(pi: PiAPI): void {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.join("; ");
|
|
107
|
-
throw new Error(`Schema validation failed: ${errors}`);
|
|
108
|
-
}
|
|
109
|
-
// Mark success for enforcement check
|
|
110
|
-
lastCallSucceeded = true;
|
|
111
|
-
return {
|
|
112
|
-
content: [
|
|
113
|
-
{ type: "text" as const, text: "Structured output recorded successfully." },
|
|
114
|
-
],
|
|
115
|
-
details: params,
|
|
116
|
-
};
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// System prompt injection with embedded schema
|
|
121
|
-
pi.on("before_agent_start", async (_event: unknown, ctx: { addSystemInstruction: (s: string) => void }) => {
|
|
122
|
-
ctx.addSystemInstruction(systemPrompt);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Track calls — count for retry cap, success for enforcement
|
|
126
|
-
pi.on("tool_execution_start", async (event: { toolName: string }) => {
|
|
127
|
-
if (event.toolName === TOOL_NAME) {
|
|
128
|
-
turnCallCount++;
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
pi.on("turn_end", async () => {
|
|
133
|
-
if (lastCallSucceeded) {
|
|
134
|
-
// Reset for next potential turn (shouldn't happen, but defensive)
|
|
135
|
-
lastCallSucceeded = false;
|
|
136
|
-
turnCallCount = 0;
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Retry cap: stop enforcement after MAX_RETRIES attempts
|
|
141
|
-
if (turnCallCount >= MAX_RETRIES) {
|
|
142
|
-
console.error(
|
|
143
|
-
`[${TOOL_NAME}] Max retries (${MAX_RETRIES}) reached without valid output. Giving up enforcement.`,
|
|
144
|
-
);
|
|
145
|
-
turnCallCount = 0;
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
pi.sendUserMessage(ENFORCEMENT_MESSAGE);
|
|
150
|
-
});
|
|
36
|
+
pi.registerTool({
|
|
37
|
+
name: TOOL_NAME,
|
|
38
|
+
label: "Structured Output",
|
|
39
|
+
description:
|
|
40
|
+
"Return structured output validated against a JSON Schema. "
|
|
41
|
+
+ "Call this tool when you need to produce structured data (JSON). "
|
|
42
|
+
+ "Pass the `schema` (JSON Schema object) and `data` (the value to validate). "
|
|
43
|
+
+ "The tool validates `data` against `schema` and returns it on success.",
|
|
44
|
+
promptSnippet:
|
|
45
|
+
"Use structured-output to return validated JSON data. "
|
|
46
|
+
+ "Pass schema (JSON Schema) and data (your output). "
|
|
47
|
+
+ "Workflow scripts: always use this tool instead of raw JSON in text.",
|
|
48
|
+
promptGuidelines: [
|
|
49
|
+
"Pass a valid JSON Schema in the `schema` parameter.",
|
|
50
|
+
"Pass the data to validate in the `data` parameter.",
|
|
51
|
+
"Do not output JSON in text — use this tool instead.",
|
|
52
|
+
],
|
|
53
|
+
parameters: Type.Object({
|
|
54
|
+
schema: Type.Object({}, {
|
|
55
|
+
description: "JSON Schema object that `data` must conform to",
|
|
56
|
+
}),
|
|
57
|
+
data: Type.Any({
|
|
58
|
+
description: "The value to validate against `schema`",
|
|
59
|
+
}),
|
|
60
|
+
}),
|
|
61
|
+
async execute(
|
|
62
|
+
_toolCallId: string,
|
|
63
|
+
params: { schema: Record<string, unknown>; data: unknown },
|
|
64
|
+
) {
|
|
65
|
+
const { schema, data } = params;
|
|
66
|
+
|
|
67
|
+
let validate: ValidateFunction;
|
|
68
|
+
try {
|
|
69
|
+
validate = getOrCompileValidator(schema);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
throw new Error(`Invalid JSON Schema: ${(e as Error).message}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const valid = validate(data);
|
|
75
|
+
if (!valid) {
|
|
76
|
+
const errors = validate.errors
|
|
77
|
+
?.map((err) => `${err.instancePath} ${err.message}`)
|
|
78
|
+
.join("; ");
|
|
79
|
+
throw new Error(`Schema validation failed: ${errors}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
content: [
|
|
84
|
+
{ type: "text" as const, text: "Structured output recorded successfully." },
|
|
85
|
+
],
|
|
86
|
+
details: data as Record<string, unknown>,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
});
|
|
151
90
|
}
|