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.
Files changed (65) hide show
  1. package/README.md +134 -97
  2. package/dist/src/aggregator.d.ts +11 -4
  3. package/dist/src/aggregator.js +108 -121
  4. package/dist/src/classifiers/{custom/context_shift → context_shift}/manifest.json +6 -11
  5. package/dist/src/classifiers/{custom/context_shift → context_shift}/prompt.md +1 -1
  6. package/dist/src/classifiers/{custom/conversation_digest → conversation_digest}/manifest.json +7 -12
  7. package/dist/src/classifiers/{custom/conversation_digest → conversation_digest}/prompt.md +2 -2
  8. package/dist/src/classifiers/{custom/memory_retrieval_queries → memory_retrieval_queries}/manifest.json +6 -11
  9. package/dist/src/classifiers/{custom/memory_retrieval_queries → memory_retrieval_queries}/prompt.md +2 -2
  10. package/dist/src/classifiers/{stock/model_specialization → model_specialization}/manifest.json +2 -2
  11. package/dist/src/classifiers/model_specialization/prompt.md +5 -0
  12. package/dist/src/classifiers/preflight/manifest.json +34 -0
  13. package/dist/src/classifiers/preflight/prompt.md +10 -0
  14. package/dist/src/classifiers/{stock/prompt_injection → prompt_injection}/manifest.json +6 -2
  15. package/dist/src/classifiers/prompt_injection/prompt.md +14 -0
  16. package/dist/src/classifiers/{stock/routing → routing}/manifest.json +2 -2
  17. package/dist/src/classifiers/routing/prompt.md +5 -0
  18. package/dist/src/classifiers/{stock/tools → tools}/manifest.json +3 -3
  19. package/dist/src/classifiers/tools/prompt.md +5 -0
  20. package/dist/src/classifiers.js +31 -32
  21. package/dist/src/classify.d.ts +10 -2
  22. package/dist/src/classify.js +27 -12
  23. package/dist/src/config.d.ts +1 -4
  24. package/dist/src/config.js +7 -45
  25. package/dist/src/index.d.ts +1 -0
  26. package/dist/src/index.js +1 -0
  27. package/dist/src/input.d.ts +4 -1
  28. package/dist/src/input.js +12 -10
  29. package/dist/src/manifest.d.ts +18 -46
  30. package/dist/src/manifest.js +1 -5
  31. package/dist/src/pipeline.d.ts +11 -2
  32. package/dist/src/pipeline.js +98 -168
  33. package/dist/src/reserved-fields.d.ts +18 -0
  34. package/dist/src/reserved-fields.js +175 -0
  35. package/dist/src/stock-prompt.d.ts +9 -2
  36. package/dist/src/stock-prompt.js +165 -45
  37. package/dist/src/stock-validation.d.ts +16 -17
  38. package/dist/src/stock-validation.js +263 -236
  39. package/dist/src/stock.d.ts +26 -62
  40. package/dist/src/stock.js +7 -14
  41. package/docs/adding-a-classifier.md +74 -32
  42. package/docs/manifests.md +112 -71
  43. package/docs/resolver.md +25 -34
  44. package/docs/signals.md +39 -58
  45. package/open-classify.config.example.json +10 -13
  46. package/package.json +1 -3
  47. package/dist/src/classifiers/stock/preflight/manifest.json +0 -11
  48. package/dist/src/classifiers/stock/prompts/classifier-header.md +0 -4
  49. package/dist/src/classifiers/stock/prompts/custom-output.md +0 -7
  50. package/dist/src/classifiers/stock/prompts/model_specialization.md +0 -7
  51. package/dist/src/classifiers/stock/prompts/preflight-output.md +0 -10
  52. package/dist/src/classifiers/stock/prompts/preflight.md +0 -47
  53. package/dist/src/classifiers/stock/prompts/prompt-injection-output.md +0 -5
  54. package/dist/src/classifiers/stock/prompts/prompt_injection.md +0 -24
  55. package/dist/src/classifiers/stock/prompts/routing-output.md +0 -5
  56. package/dist/src/classifiers/stock/prompts/routing.md +0 -9
  57. package/dist/src/classifiers/stock/prompts/specialty.md +0 -12
  58. package/dist/src/classifiers/stock/prompts/tier.md +0 -7
  59. package/dist/src/classifiers/stock/prompts/tools-output.md +0 -11
  60. package/dist/src/classifiers/stock/prompts/tools.md +0 -10
  61. package/dist/src/ui-server.d.ts +0 -1
  62. package/dist/src/ui-server.js +0 -257
  63. /package/dist/src/classifiers/{stock/prompts → _prompts}/base.md +0 -0
  64. /package/dist/src/classifiers/{stock/prompts → _prompts}/confidence.md +0 -0
  65. /package/dist/src/classifiers/{stock/prompts → _prompts}/reason.md +0 -0
@@ -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
- }