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/ui-server.js
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
// A tiny dev/demo HTTP server backing the bundled UI. Two responsibilities:
|
|
2
|
-
// 1. Serve the static UI from `./ui` (HTML, CSS, JS).
|
|
3
|
-
// 2. Run a classification over Server-Sent Events at /api/classify-stream.
|
|
4
|
-
//
|
|
5
|
-
// The SSE event vocabulary the UI listens for:
|
|
6
|
-
// pipeline_started — pipeline boot, includes the classifier list
|
|
7
|
-
// pipeline_phase — coarse phase ("normalizing" / "resource_check" /
|
|
8
|
-
// "running"); useful for progress UI
|
|
9
|
-
// classifier_started — a specific classifier is now running
|
|
10
|
-
// classifier_completed — that classifier returned a model result
|
|
11
|
-
// classifier_failed — that classifier threw without being aborted
|
|
12
|
-
// classifier_aborted — early-exit short-circuit cancelled this classifier
|
|
13
|
-
// classifier_timed_out — the per-classifier timeout fired
|
|
14
|
-
// pipeline_completed — final PipelineResult payload
|
|
15
|
-
// pipeline_failed — pipeline-level error (normalization, etc.)
|
|
16
|
-
//
|
|
17
|
-
// This server is intentionally minimal — no auth, no rate limiting, binds to
|
|
18
|
-
// 127.0.0.1 by default. It is not meant for production.
|
|
19
|
-
import { createReadStream, existsSync } from "node:fs";
|
|
20
|
-
import { createServer } from "node:http";
|
|
21
|
-
import { extname, join, normalize } from "node:path";
|
|
22
|
-
import { loadCatalog } from "./catalog.js";
|
|
23
|
-
import { CLASSIFIER_NAMES, REGISTRY } from "./classifiers.js";
|
|
24
|
-
import { DEFAULT_CERTAINTY_THRESHOLD, certaintyThreshold, } from "./aggregator.js";
|
|
25
|
-
import { classifierModelsFromConfig, loadOpenClassifyConfig, } from "./config.js";
|
|
26
|
-
import { DEFAULT_CERTAINTY_GATE } from "./pipeline.js";
|
|
27
|
-
import { DOWNSTREAM_MODEL_TIER_VALUES, MODEL_SPECIALIZATION_VALUES, PROMPT_INJECTION_RISK_LEVEL_VALUES, } from "./enums.js";
|
|
28
|
-
import { createOllamaClassifierRunner, OLLAMA_CONTEXT_LENGTH, OLLAMA_DEFAULT_CATALOG_PATH, OLLAMA_MIN_AVAILABLE_MEMORY_BYTES, OLLAMA_MIN_TOTAL_MEMORY_BYTES, OLLAMA_REQUIRED_PARALLELISM, } from "./ollama.js";
|
|
29
|
-
import { classifyOpenClassifyInput } from "./pipeline.js";
|
|
30
|
-
// Served at GET /api/enums so the UI never needs to duplicate shared enum values.
|
|
31
|
-
const CLASSIFIER_ENUMS = {
|
|
32
|
-
downstream_model_tier: [...DOWNSTREAM_MODEL_TIER_VALUES],
|
|
33
|
-
model_specialization: [...MODEL_SPECIALIZATION_VALUES],
|
|
34
|
-
prompt_injection_risk_level: [...PROMPT_INJECTION_RISK_LEVEL_VALUES],
|
|
35
|
-
};
|
|
36
|
-
const CLASSIFIER_METADATA = REGISTRY.map((classifier) => ({
|
|
37
|
-
name: classifier.name,
|
|
38
|
-
kind: classifier.kind,
|
|
39
|
-
version: classifier.version,
|
|
40
|
-
purpose: classifier.purpose,
|
|
41
|
-
order: classifier.order,
|
|
42
|
-
...("tools" in classifier ? { tools: classifier.tools ?? [] } : {}),
|
|
43
|
-
}));
|
|
44
|
-
const PORT = Number(process.env.OPEN_CLASSIFY_UI_PORT ?? 4317);
|
|
45
|
-
const HOST = process.env.OPEN_CLASSIFY_UI_HOST ?? "127.0.0.1";
|
|
46
|
-
const UI_DIR = join(process.cwd(), "ui");
|
|
47
|
-
const OPEN_CLASSIFY_CONFIG = loadOpenClassifyConfig(undefined, {
|
|
48
|
-
optional: process.env.OPEN_CLASSIFY_CONFIG === undefined,
|
|
49
|
-
});
|
|
50
|
-
const CATALOG_PATH = process.env.OPEN_CLASSIFY_CATALOG_PATH ??
|
|
51
|
-
OPEN_CLASSIFY_CONFIG?.catalog ??
|
|
52
|
-
OLLAMA_DEFAULT_CATALOG_PATH;
|
|
53
|
-
const MIME_TYPES = {
|
|
54
|
-
".html": "text/html; charset=utf-8",
|
|
55
|
-
".css": "text/css; charset=utf-8",
|
|
56
|
-
".js": "text/javascript; charset=utf-8",
|
|
57
|
-
".json": "application/json; charset=utf-8",
|
|
58
|
-
};
|
|
59
|
-
const server = createServer((request, response) => {
|
|
60
|
-
void route(request, response);
|
|
61
|
-
});
|
|
62
|
-
server.listen(PORT, HOST, () => {
|
|
63
|
-
console.log(`Open Classify UI running at http://${HOST}:${PORT}/`);
|
|
64
|
-
});
|
|
65
|
-
async function route(request, response) {
|
|
66
|
-
const startedAt = Date.now();
|
|
67
|
-
console.log(`[req] ${request.method} ${request.url}`);
|
|
68
|
-
try {
|
|
69
|
-
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
70
|
-
if (request.method === "POST" && url.pathname === "/api/classify-stream") {
|
|
71
|
-
await classifyStream(request, response);
|
|
72
|
-
console.log(`[req] ${request.method} ${request.url} stream ended in ${Date.now() - startedAt}ms`);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (request.method === "GET" && url.pathname === "/api/enums") {
|
|
76
|
-
sendJson(response, CLASSIFIER_ENUMS);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (request.method === "GET" && url.pathname === "/api/classifiers") {
|
|
80
|
-
sendJson(response, {
|
|
81
|
-
classifiers: CLASSIFIER_METADATA,
|
|
82
|
-
aggregator: {
|
|
83
|
-
certaintyGate: OPEN_CLASSIFY_CONFIG?.aggregator?.certaintyGate ?? DEFAULT_CERTAINTY_GATE,
|
|
84
|
-
certaintyThreshold: certaintyThreshold(OPEN_CLASSIFY_CONFIG?.aggregator) ?? DEFAULT_CERTAINTY_THRESHOLD,
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
if (request.method === "GET") {
|
|
90
|
-
serveStatic(url.pathname, response);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
sendJson(response, { error: "method not allowed" }, 405);
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
console.error(`[req] ${request.method} ${request.url} failed:`, error);
|
|
97
|
-
sendJson(response, { error: errorMessage(error) }, 500);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
async function classifyStream(request, response) {
|
|
101
|
-
response.writeHead(200, {
|
|
102
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
103
|
-
"cache-control": "no-cache, no-transform",
|
|
104
|
-
connection: "keep-alive",
|
|
105
|
-
"x-accel-buffering": "no",
|
|
106
|
-
});
|
|
107
|
-
response.flushHeaders();
|
|
108
|
-
// Disable Nagle so each event flushes immediately. SSE is interactive;
|
|
109
|
-
// batching kills the "live" feel.
|
|
110
|
-
request.socket.setNoDelay(true);
|
|
111
|
-
let closed = false;
|
|
112
|
-
const clientAbortController = new AbortController();
|
|
113
|
-
const abortForClientClose = () => {
|
|
114
|
-
closed = true;
|
|
115
|
-
clientAbortController.abort(new Error("SSE client disconnected"));
|
|
116
|
-
};
|
|
117
|
-
response.on("close", () => {
|
|
118
|
-
abortForClientClose();
|
|
119
|
-
});
|
|
120
|
-
response.on("error", () => {
|
|
121
|
-
abortForClientClose();
|
|
122
|
-
});
|
|
123
|
-
const send = (event, data) => {
|
|
124
|
-
if (closed || response.writableEnded || response.destroyed) {
|
|
125
|
-
console.warn(`[sse] dropped ${event} (closed=${closed} ended=${response.writableEnded})`);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const ok = response.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
129
|
-
console.log(`[sse] -> ${event}${data?.name ? ` ${data.name}` : ""}${ok ? "" : " [backpressure]"}`);
|
|
130
|
-
};
|
|
131
|
-
// SSE comment heartbeat. Some intermediaries (proxies, load balancers)
|
|
132
|
-
// close idle connections; a tiny ping every 5s keeps the stream warm.
|
|
133
|
-
// The leading `:` makes browsers ignore the line as a comment.
|
|
134
|
-
const heartbeat = setInterval(() => {
|
|
135
|
-
if (closed || response.writableEnded || response.destroyed) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
response.write(`: ping ${Date.now()}\n\n`);
|
|
139
|
-
}, 5000);
|
|
140
|
-
try {
|
|
141
|
-
const input = (await readJsonBody(request));
|
|
142
|
-
const baseRunner = createOllamaClassifierRunner({
|
|
143
|
-
host: OPEN_CLASSIFY_CONFIG?.runner?.host,
|
|
144
|
-
defaultModel: OPEN_CLASSIFY_CONFIG?.runner?.defaultModel,
|
|
145
|
-
models: classifierModelsFromConfig(OPEN_CLASSIFY_CONFIG),
|
|
146
|
-
options: OPEN_CLASSIFY_CONFIG?.runner?.options,
|
|
147
|
-
});
|
|
148
|
-
const runClassifier = async (name, classifierInput, signal) => {
|
|
149
|
-
send("classifier_started", { name, started_at: Date.now() });
|
|
150
|
-
try {
|
|
151
|
-
const result = await baseRunner(name, classifierInput, signal);
|
|
152
|
-
send("classifier_completed", { name, result, completed_at: Date.now() });
|
|
153
|
-
return result;
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
console.error(`[classifier] ${name} threw:`, error);
|
|
157
|
-
if (signal.aborted) {
|
|
158
|
-
send(isTimeoutAbort(name, signal) ? "classifier_timed_out" : "classifier_aborted", {
|
|
159
|
-
name,
|
|
160
|
-
reason: errorMessage(signal.reason ?? error),
|
|
161
|
-
completed_at: Date.now(),
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
send("classifier_failed", {
|
|
166
|
-
name,
|
|
167
|
-
error: errorMessage(error),
|
|
168
|
-
completed_at: Date.now(),
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
throw error;
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
send("pipeline_started", {
|
|
175
|
-
classifiers: CLASSIFIER_NAMES,
|
|
176
|
-
started_at: Date.now(),
|
|
177
|
-
});
|
|
178
|
-
send("pipeline_phase", { phase: "normalizing" });
|
|
179
|
-
send("pipeline_phase", {
|
|
180
|
-
phase: "resource_check",
|
|
181
|
-
required_parallelism: OLLAMA_REQUIRED_PARALLELISM,
|
|
182
|
-
context_length: OLLAMA_CONTEXT_LENGTH,
|
|
183
|
-
min_total_memory_bytes: OLLAMA_MIN_TOTAL_MEMORY_BYTES,
|
|
184
|
-
min_available_memory_bytes: OLLAMA_MIN_AVAILABLE_MEMORY_BYTES,
|
|
185
|
-
});
|
|
186
|
-
send("pipeline_phase", { phase: "running" });
|
|
187
|
-
const result = await classifyOpenClassifyInput(input, {
|
|
188
|
-
runClassifier,
|
|
189
|
-
catalog: loadCatalog(CATALOG_PATH),
|
|
190
|
-
aggregator: OPEN_CLASSIFY_CONFIG?.aggregator,
|
|
191
|
-
signal: clientAbortController.signal,
|
|
192
|
-
});
|
|
193
|
-
send("pipeline_completed", result);
|
|
194
|
-
}
|
|
195
|
-
catch (error) {
|
|
196
|
-
console.error("[pipeline] failed:", error);
|
|
197
|
-
send("pipeline_failed", { error: errorMessage(error) });
|
|
198
|
-
}
|
|
199
|
-
finally {
|
|
200
|
-
clearInterval(heartbeat);
|
|
201
|
-
closed = true;
|
|
202
|
-
if (!response.writableEnded && !response.destroyed) {
|
|
203
|
-
response.end();
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
// Distinguishes a timeout-driven abort from a pipeline early-exit abort, so
|
|
208
|
-
// the UI can show the right state. We sniff the abort reason's message
|
|
209
|
-
// because that's the only signal the pipeline gives us — it doesn't tag
|
|
210
|
-
// reasons with a structured discriminator.
|
|
211
|
-
function isTimeoutAbort(name, signal) {
|
|
212
|
-
return errorMessage(signal.reason).includes(`${name} classifier timed out`);
|
|
213
|
-
}
|
|
214
|
-
function serveStatic(pathname, response) {
|
|
215
|
-
const requestedPath = pathname === "/" ? "/index.html" : pathname;
|
|
216
|
-
// Two-layer path-traversal guard: strip leading `../` segments from the
|
|
217
|
-
// normalized path, then double-check the resolved file is still inside
|
|
218
|
-
// UI_DIR. The redundancy is intentional — defense in depth on a static
|
|
219
|
-
// file server is cheap.
|
|
220
|
-
const safePath = normalize(requestedPath).replace(/^(\.\.[/\\])+/, "");
|
|
221
|
-
const filePath = join(UI_DIR, safePath);
|
|
222
|
-
if (!filePath.startsWith(UI_DIR) || !existsSync(filePath)) {
|
|
223
|
-
sendJson(response, { error: "not found" }, 404);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
response.writeHead(200, {
|
|
227
|
-
"content-type": MIME_TYPES[extname(filePath)] ?? "application/octet-stream",
|
|
228
|
-
"cache-control": "no-store",
|
|
229
|
-
});
|
|
230
|
-
createReadStream(filePath).on("error", () => response.destroy()).pipe(response);
|
|
231
|
-
}
|
|
232
|
-
function sendJson(response, data, status = 200) {
|
|
233
|
-
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
234
|
-
response.end(JSON.stringify(data));
|
|
235
|
-
}
|
|
236
|
-
// 512 KiB cap matches the input contract (5,000-char message budget plus
|
|
237
|
-
// generous slack for history). Big enough for any legitimate
|
|
238
|
-
// classification request, small enough to not be a DoS vector.
|
|
239
|
-
async function readJsonBody(request) {
|
|
240
|
-
const chunks = [];
|
|
241
|
-
let size = 0;
|
|
242
|
-
for await (const chunk of request) {
|
|
243
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
244
|
-
size += buffer.byteLength;
|
|
245
|
-
if (size > 512 * 1024) {
|
|
246
|
-
throw new Error("request body is too large");
|
|
247
|
-
}
|
|
248
|
-
chunks.push(buffer);
|
|
249
|
-
}
|
|
250
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
251
|
-
}
|
|
252
|
-
function errorMessage(error) {
|
|
253
|
-
if (error instanceof Error) {
|
|
254
|
-
return error.message;
|
|
255
|
-
}
|
|
256
|
-
return String(error);
|
|
257
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|