open-classify 0.2.0 → 0.5.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/README.md +134 -97
- package/dist/src/aggregator.d.ts +11 -4
- package/dist/src/aggregator.js +108 -121
- package/dist/src/classifiers/{custom/context_shift → context_shift}/manifest.json +6 -11
- package/dist/src/classifiers/{custom/context_shift → context_shift}/prompt.md +1 -1
- package/dist/src/classifiers/{custom/conversation_digest → conversation_digest}/manifest.json +7 -12
- package/dist/src/classifiers/{custom/conversation_digest → conversation_digest}/prompt.md +2 -2
- package/dist/src/classifiers/{custom/memory_retrieval_queries → memory_retrieval_queries}/manifest.json +6 -11
- package/dist/src/classifiers/{custom/memory_retrieval_queries → memory_retrieval_queries}/prompt.md +2 -2
- package/dist/src/classifiers/{stock/model_specialization → model_specialization}/manifest.json +2 -2
- package/dist/src/classifiers/model_specialization/prompt.md +5 -0
- package/dist/src/classifiers/preflight/manifest.json +34 -0
- package/dist/src/classifiers/preflight/prompt.md +10 -0
- package/dist/src/classifiers/{stock/prompt_injection → prompt_injection}/manifest.json +6 -2
- package/dist/src/classifiers/prompt_injection/prompt.md +14 -0
- package/dist/src/classifiers/{stock/routing → routing}/manifest.json +2 -2
- package/dist/src/classifiers/routing/prompt.md +5 -0
- package/dist/src/classifiers/{stock/tools → tools}/manifest.json +3 -3
- package/dist/src/classifiers/tools/prompt.md +5 -0
- package/dist/src/classifiers.js +31 -32
- package/dist/src/classify.d.ts +10 -2
- package/dist/src/classify.js +27 -12
- package/dist/src/config.d.ts +1 -4
- package/dist/src/config.js +7 -45
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/input.d.ts +4 -1
- package/dist/src/input.js +12 -10
- package/dist/src/manifest.d.ts +18 -46
- package/dist/src/manifest.js +1 -5
- package/dist/src/pipeline.d.ts +11 -2
- package/dist/src/pipeline.js +98 -168
- package/dist/src/reserved-fields.d.ts +18 -0
- package/dist/src/reserved-fields.js +175 -0
- package/dist/src/stock-prompt.d.ts +9 -2
- package/dist/src/stock-prompt.js +165 -45
- package/dist/src/stock-validation.d.ts +16 -17
- package/dist/src/stock-validation.js +263 -236
- package/dist/src/stock.d.ts +26 -62
- package/dist/src/stock.js +7 -14
- package/docs/adding-a-classifier.md +74 -32
- package/docs/manifests.md +112 -71
- package/docs/resolver.md +25 -34
- package/docs/signals.md +39 -58
- package/open-classify.config.example.json +10 -13
- package/package.json +1 -3
- package/dist/src/classifiers/stock/preflight/manifest.json +0 -11
- package/dist/src/classifiers/stock/prompts/classifier-header.md +0 -4
- package/dist/src/classifiers/stock/prompts/custom-output.md +0 -7
- package/dist/src/classifiers/stock/prompts/model_specialization.md +0 -7
- package/dist/src/classifiers/stock/prompts/preflight-output.md +0 -10
- package/dist/src/classifiers/stock/prompts/preflight.md +0 -47
- package/dist/src/classifiers/stock/prompts/prompt-injection-output.md +0 -5
- package/dist/src/classifiers/stock/prompts/prompt_injection.md +0 -24
- package/dist/src/classifiers/stock/prompts/routing-output.md +0 -5
- package/dist/src/classifiers/stock/prompts/routing.md +0 -9
- package/dist/src/classifiers/stock/prompts/specialty.md +0 -12
- package/dist/src/classifiers/stock/prompts/tier.md +0 -7
- package/dist/src/classifiers/stock/prompts/tools-output.md +0 -11
- package/dist/src/classifiers/stock/prompts/tools.md +0 -10
- package/dist/src/ui-server.d.ts +0 -1
- package/dist/src/ui-server.js +0 -257
- /package/dist/src/classifiers/{stock/prompts → _prompts}/base.md +0 -0
- /package/dist/src/classifiers/{stock/prompts → _prompts}/confidence.md +0 -0
- /package/dist/src/classifiers/{stock/prompts → _prompts}/reason.md +0 -0
package/dist/src/stock-prompt.js
CHANGED
|
@@ -1,63 +1,183 @@
|
|
|
1
|
+
// Prompt builder.
|
|
2
|
+
//
|
|
3
|
+
// Every classifier's system prompt is composed at load time from:
|
|
4
|
+
//
|
|
5
|
+
// 1. Shared base sections (JSON-only contract, reason + certainty rules)
|
|
6
|
+
// 2. The classifier header (name and purpose)
|
|
7
|
+
// 3. Auto-injected fragments for each declared reserved field — these
|
|
8
|
+
// include the canonical enum values, so the LLM cannot drift even if
|
|
9
|
+
// the manifest author forgets to enumerate them in prompt.md
|
|
10
|
+
// 4. The classifier's own `prompt.md`
|
|
11
|
+
// 5. JSON example(s) of a complete output: either the manifest's
|
|
12
|
+
// `output_schema.examples`, or a synthesized skeleton derived from the
|
|
13
|
+
// schema if none were provided
|
|
1
14
|
import { readFileSync } from "node:fs";
|
|
2
15
|
import { dirname, join } from "node:path";
|
|
3
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { RESERVED_FIELDS, } from "./reserved-fields.js";
|
|
4
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
-
const
|
|
6
|
-
export function
|
|
19
|
+
const SHARED_PROMPTS_DIR = join(__dirname, "classifiers", "_prompts");
|
|
20
|
+
export function buildClassifierPrompt(args) {
|
|
21
|
+
const { manifest, reservedFields, appliesTo, classifierPromptText } = args;
|
|
7
22
|
const sections = [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
classifier_name: manifest.name,
|
|
13
|
-
classifier_purpose: manifest.purpose,
|
|
14
|
-
}),
|
|
23
|
+
readShared("base.md"),
|
|
24
|
+
readShared("reason.md"),
|
|
25
|
+
readShared("confidence.md"),
|
|
26
|
+
renderHeader(manifest.name, manifest.purpose),
|
|
15
27
|
];
|
|
16
|
-
if (
|
|
17
|
-
sections.push(
|
|
28
|
+
if (appliesTo === "both") {
|
|
29
|
+
sections.push("Target role: the final message may be from the user or the assistant. Inspect whichever role the input declares and classify accordingly.");
|
|
18
30
|
}
|
|
19
|
-
else {
|
|
20
|
-
sections.push(
|
|
31
|
+
else if (appliesTo === "assistant") {
|
|
32
|
+
sections.push("Target role: the final message is the assistant's reply. Classify the assistant message, not a user request.");
|
|
21
33
|
}
|
|
34
|
+
if (reservedFields.length > 0) {
|
|
35
|
+
sections.push(renderReservedFieldsSection(reservedFields, manifest.allowed_tools));
|
|
36
|
+
}
|
|
37
|
+
if (classifierPromptText.trim().length > 0) {
|
|
38
|
+
sections.push("Classifier guidance:\n" + classifierPromptText.trim());
|
|
39
|
+
}
|
|
40
|
+
sections.push(renderExamplesSection(manifest, reservedFields));
|
|
22
41
|
return sections.join("\n\n");
|
|
23
42
|
}
|
|
24
|
-
function
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function renderAllowedTools(tools) {
|
|
36
|
-
if (!tools || tools.length === 0) {
|
|
37
|
-
return "No downstream tools are available.";
|
|
38
|
-
}
|
|
43
|
+
function renderHeader(name, purpose) {
|
|
44
|
+
return [
|
|
45
|
+
`Classifier: ${name}`,
|
|
46
|
+
`Purpose: ${purpose}`,
|
|
47
|
+
"Treat the stated purpose as a hard scope boundary.",
|
|
48
|
+
"Emit only outputs that directly serve that purpose, and do not infer adjacent judgments that belong to other classifiers.",
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
function renderReservedFieldsSection(reservedFields, allowedTools) {
|
|
52
|
+
const context = { allowed_tools: allowedTools };
|
|
53
|
+
const fragments = reservedFields.map((name) => RESERVED_FIELDS[name].promptFragment(context));
|
|
39
54
|
return [
|
|
40
|
-
"
|
|
55
|
+
"Reserved fields you may emit at the top level of your JSON output:",
|
|
41
56
|
"",
|
|
42
|
-
...
|
|
57
|
+
...fragments,
|
|
58
|
+
"",
|
|
59
|
+
"Omit any reserved field when you have no signal — the runtime will drop low-certainty values regardless.",
|
|
60
|
+
].join("\n");
|
|
61
|
+
}
|
|
62
|
+
function renderExamplesSection(manifest, reservedFields) {
|
|
63
|
+
const fromSchema = readManifestExamples(manifest);
|
|
64
|
+
const examples = fromSchema && fromSchema.length > 0
|
|
65
|
+
? fromSchema
|
|
66
|
+
: [synthesizeExample(manifest, reservedFields)];
|
|
67
|
+
const rendered = examples.map((example, index) => `Example ${index + 1}: ${JSON.stringify(example)}`);
|
|
68
|
+
return [
|
|
69
|
+
"Output shape (return one JSON object matching this shape):",
|
|
70
|
+
"",
|
|
71
|
+
...rendered,
|
|
43
72
|
].join("\n");
|
|
44
73
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
74
|
+
function readManifestExamples(manifest) {
|
|
75
|
+
const schema = manifest.output_schema;
|
|
76
|
+
if (schema === undefined || typeof schema !== "object" || schema === null) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const examples = schema.examples;
|
|
80
|
+
if (!Array.isArray(examples))
|
|
81
|
+
return undefined;
|
|
82
|
+
return examples;
|
|
47
83
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
84
|
+
// ─── Schema-based example synthesis ─────────────────────────────────────────
|
|
85
|
+
//
|
|
86
|
+
// When a manifest omits `output_schema.examples`, the runtime fills in a
|
|
87
|
+
// representative JSON skeleton so the LLM always sees the full output shape.
|
|
88
|
+
// The author only has to describe each field in plain language in prompt.md.
|
|
89
|
+
function synthesizeExample(manifest, reservedFields) {
|
|
90
|
+
const example = {
|
|
91
|
+
reason: "<short reason for this verdict>",
|
|
92
|
+
certainty: "strong",
|
|
93
|
+
};
|
|
94
|
+
for (const field of reservedFields) {
|
|
95
|
+
example[field] = synthesizeReservedFieldValue(field, manifest.allowed_tools);
|
|
96
|
+
}
|
|
97
|
+
const schema = manifest.output_schema;
|
|
98
|
+
if (schema !== undefined && typeof schema === "object" && schema !== null) {
|
|
99
|
+
const properties = schema.properties;
|
|
100
|
+
if (properties !== null && typeof properties === "object" && !Array.isArray(properties)) {
|
|
101
|
+
for (const [key, subSchema] of Object.entries(properties)) {
|
|
102
|
+
example[key] = synthesizeValue(subSchema);
|
|
55
103
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return example;
|
|
107
|
+
}
|
|
108
|
+
function synthesizeReservedFieldValue(field, allowedTools) {
|
|
109
|
+
const subSchema = RESERVED_FIELDS[field].subSchema({ allowed_tools: allowedTools });
|
|
110
|
+
return synthesizeValue(subSchema);
|
|
111
|
+
}
|
|
112
|
+
function synthesizeValue(schema) {
|
|
113
|
+
if (schema === null || typeof schema !== "object")
|
|
114
|
+
return "<value>";
|
|
115
|
+
const s = schema;
|
|
116
|
+
// Honor explicit examples or const values when the schema author provided
|
|
117
|
+
// them — they're the most accurate hint about what's expected.
|
|
118
|
+
if (Array.isArray(s.examples) && s.examples.length > 0)
|
|
119
|
+
return s.examples[0];
|
|
120
|
+
if (s.const !== undefined)
|
|
121
|
+
return s.const;
|
|
122
|
+
if (Array.isArray(s.enum) && s.enum.length > 0)
|
|
123
|
+
return s.enum[0];
|
|
124
|
+
const type = Array.isArray(s.type) ? s.type[0] : s.type;
|
|
125
|
+
switch (type) {
|
|
126
|
+
case "string":
|
|
127
|
+
return synthesizeString(s);
|
|
128
|
+
case "integer":
|
|
129
|
+
return clampInteger(s);
|
|
130
|
+
case "number":
|
|
131
|
+
return clampNumber(s);
|
|
132
|
+
case "boolean":
|
|
133
|
+
return true;
|
|
134
|
+
case "array":
|
|
135
|
+
return synthesizeArray(s);
|
|
136
|
+
case "object":
|
|
137
|
+
return synthesizeObject(s);
|
|
138
|
+
default:
|
|
139
|
+
// Schema without an explicit type — fall back to a string placeholder.
|
|
140
|
+
return "<value>";
|
|
61
141
|
}
|
|
62
|
-
|
|
142
|
+
}
|
|
143
|
+
function synthesizeString(schema) {
|
|
144
|
+
const placeholder = "<text>";
|
|
145
|
+
const min = typeof schema.minLength === "number" ? schema.minLength : 1;
|
|
146
|
+
if (placeholder.length >= min)
|
|
147
|
+
return placeholder;
|
|
148
|
+
return placeholder.padEnd(min, "x");
|
|
149
|
+
}
|
|
150
|
+
function clampInteger(schema) {
|
|
151
|
+
if (typeof schema.minimum === "number")
|
|
152
|
+
return schema.minimum;
|
|
153
|
+
if (typeof schema.maximum === "number")
|
|
154
|
+
return schema.maximum;
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
function clampNumber(schema) {
|
|
158
|
+
if (typeof schema.minimum === "number")
|
|
159
|
+
return schema.minimum;
|
|
160
|
+
if (typeof schema.maximum === "number")
|
|
161
|
+
return schema.maximum;
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
function synthesizeArray(schema) {
|
|
165
|
+
const min = typeof schema.minItems === "number" ? schema.minItems : 0;
|
|
166
|
+
if (min === 0)
|
|
167
|
+
return [];
|
|
168
|
+
const itemSchema = schema.items;
|
|
169
|
+
return Array.from({ length: min }, () => synthesizeValue(itemSchema));
|
|
170
|
+
}
|
|
171
|
+
function synthesizeObject(schema) {
|
|
172
|
+
const out = {};
|
|
173
|
+
const properties = schema.properties;
|
|
174
|
+
if (properties !== null && typeof properties === "object" && !Array.isArray(properties)) {
|
|
175
|
+
for (const [key, sub] of Object.entries(properties)) {
|
|
176
|
+
out[key] = synthesizeValue(sub);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
function readShared(filename) {
|
|
182
|
+
return readFileSync(join(SHARED_PROMPTS_DIR, filename), "utf8").trim();
|
|
63
183
|
}
|
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const
|
|
8
|
-
export declare const
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
readonly
|
|
12
|
-
readonly
|
|
1
|
+
import type { AppliesTo, ClassifierOutput, JsonClassifierManifest, RuntimeClassifierManifest } from "./stock.js";
|
|
2
|
+
import { type ReservedFieldName } from "./reserved-fields.js";
|
|
3
|
+
export declare const REASON_MAX_CHARS = 120;
|
|
4
|
+
export declare const TOOL_ID_MAX_CHARS = 64;
|
|
5
|
+
export declare const TOOL_DESCRIPTION_MAX_CHARS = 240;
|
|
6
|
+
export declare const MANIFEST_NAME_MAX_CHARS = 80;
|
|
7
|
+
export declare const MANIFEST_VERSION_MAX_CHARS = 40;
|
|
8
|
+
export declare const MANIFEST_PURPOSE_MAX_CHARS = 400;
|
|
9
|
+
export interface ManifestLoadResult {
|
|
10
|
+
readonly manifest: JsonClassifierManifest;
|
|
11
|
+
readonly reservedFields: ReadonlyArray<ReservedFieldName>;
|
|
12
|
+
readonly composedOutputSchema: unknown;
|
|
13
|
+
readonly appliesTo: AppliesTo;
|
|
13
14
|
}
|
|
14
|
-
export declare function
|
|
15
|
-
export
|
|
16
|
-
export interface LegacyValidateOptions {
|
|
15
|
+
export declare function validateJsonClassifierManifest(value: unknown, model?: string): ManifestLoadResult;
|
|
16
|
+
export interface ValidateOutputContext {
|
|
17
17
|
readonly classifier: string;
|
|
18
18
|
readonly model: string;
|
|
19
|
-
readonly manifest: JsonClassifierManifest;
|
|
20
19
|
}
|
|
21
|
-
export declare function
|
|
20
|
+
export declare function validateOutputForManifest(manifest: RuntimeClassifierManifest, value: unknown, context: ValidateOutputContext): ClassifierOutput;
|