open-classify 0.9.2 → 1.0.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/CHANGELOG.md +89 -0
- package/README.md +86 -137
- package/bin/open-classify.mjs +268 -650
- package/dist/src/classifiers.js +25 -18
- package/dist/src/config.d.ts +2 -2
- package/dist/src/config.js +18 -16
- package/docs/adding-a-classifier.md +15 -14
- package/docs/manifests.md +1 -1
- package/package.json +2 -3
- package/templates/scaffold/open-classify/README.md +46 -0
- package/templates/scaffold/open-classify/classifiers/README.md +22 -0
- package/templates/scaffold/open-classify/config.json +12 -0
- package/open-classify.config.example.json +0 -26
- /package/{downstream-models.json → templates/scaffold/open-classify/downstream-models.json} +0 -0
- /package/templates/{context_shift → stock/context_shift}/manifest.json +0 -0
- /package/templates/{context_shift → stock/context_shift}/prompt.md +0 -0
- /package/templates/{conversation_digest → stock/conversation_digest}/manifest.json +0 -0
- /package/templates/{conversation_digest → stock/conversation_digest}/prompt.md +0 -0
- /package/templates/{memory_retrieval_queries → stock/memory_retrieval_queries}/manifest.json +0 -0
- /package/templates/{memory_retrieval_queries → stock/memory_retrieval_queries}/prompt.md +0 -0
- /package/templates/{tools → stock/tools}/manifest.json +0 -0
- /package/templates/{tools → stock/tools}/prompt.md +0 -0
package/dist/src/classifiers.js
CHANGED
|
@@ -13,11 +13,11 @@ export const STOCK_CLASSIFIER_NAMES = [
|
|
|
13
13
|
];
|
|
14
14
|
export const STOCK_CLASSIFIERS_DIR = findStockClassifiersDir();
|
|
15
15
|
function findStockClassifiersDir() {
|
|
16
|
-
// Source runs use ../templates; built package runs use ../../templates
|
|
17
|
-
// dist/src. Keep both so tests and the published package agree.
|
|
16
|
+
// Source runs use ../templates/stock; built package runs use ../../templates/stock
|
|
17
|
+
// from dist/src. Keep both so tests and the published package agree.
|
|
18
18
|
const candidates = [
|
|
19
|
-
join(__dirname, "..", "templates"),
|
|
20
|
-
join(__dirname, "..", "..", "templates"),
|
|
19
|
+
join(__dirname, "..", "templates", "stock"),
|
|
20
|
+
join(__dirname, "..", "..", "templates", "stock"),
|
|
21
21
|
];
|
|
22
22
|
return candidates.find((dir) => existsSync(dir)) ?? candidates[0];
|
|
23
23
|
}
|
|
@@ -50,21 +50,28 @@ export function loadClassifierRegistry(classifiersDir = BUILTIN_CLASSIFIERS_DIR)
|
|
|
50
50
|
return manifests;
|
|
51
51
|
}
|
|
52
52
|
// Build a complete classifier registry from the bundled built-ins plus any
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// names.
|
|
53
|
+
// extras supplied by the caller. Sorted by dispatch_order ascending
|
|
54
|
+
// (manifests without dispatch_order sort last).
|
|
56
55
|
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// `
|
|
56
|
+
// Precedence rules:
|
|
57
|
+
// - Mandatory built-ins (preflight, model_tier, model_specialization,
|
|
58
|
+
// prompt_injection) always load. A user classifier with the same name
|
|
59
|
+
// as a built-in throws — to replace behaviour, use a custom RunClassifier.
|
|
60
|
+
// - User classifiers in `extraDirs` override stock classifiers of the
|
|
61
|
+
// same name. This is the "eject" pattern: `npx open-classify eject
|
|
62
|
+
// tools` copies the stock files into your project, and the runtime
|
|
63
|
+
// transparently switches to your copy.
|
|
64
|
+
// - Two user classifiers with the same name (across different extraDirs)
|
|
65
|
+
// throw — ambiguous ownership.
|
|
62
66
|
export function buildClassifierRegistry(options = {}) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
]
|
|
67
|
+
const builtIns = loadClassifierRegistry(BUILTIN_CLASSIFIERS_DIR);
|
|
68
|
+
const userClassifiers = (options.extraDirs ?? []).flatMap((dir) => loadClassifierRegistry(dir));
|
|
69
|
+
const userNames = new Set(userClassifiers.map((m) => m.name));
|
|
70
|
+
// Skip any stock classifier the user has ejected (matched by name).
|
|
71
|
+
const stockManifests = (options.stockClassifierNames ?? [])
|
|
72
|
+
.filter((name) => !userNames.has(name))
|
|
73
|
+
.map((name) => loadStockClassifier(name));
|
|
74
|
+
const manifests = [...builtIns, ...stockManifests, ...userClassifiers];
|
|
68
75
|
manifests.sort((a, b) => (a.dispatch_order ?? Infinity) - (b.dispatch_order ?? Infinity));
|
|
69
76
|
validateRegistry(manifests);
|
|
70
77
|
const registry = manifests;
|
|
@@ -115,7 +122,7 @@ function validateRegistry(manifests) {
|
|
|
115
122
|
const names = new Set();
|
|
116
123
|
for (const manifest of manifests) {
|
|
117
124
|
if (names.has(manifest.name)) {
|
|
118
|
-
throw new ClassifierManifestError(`duplicate classifier name: ${manifest.name}
|
|
125
|
+
throw new ClassifierManifestError(`duplicate classifier name: "${manifest.name}". A user classifier cannot override a mandatory built-in (preflight, model_tier, model_specialization, prompt_injection), and no two user classifiers may share a name. To replace a built-in's behaviour, pass a custom RunClassifier.`);
|
|
119
126
|
}
|
|
120
127
|
names.add(manifest.name);
|
|
121
128
|
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ClassifierName } from "./classifiers.js";
|
|
2
|
-
export declare const DEFAULT_OPEN_CLASSIFY_CONFIG_PATH = "open-classify
|
|
2
|
+
export declare const DEFAULT_OPEN_CLASSIFY_CONFIG_PATH = "open-classify/config.json";
|
|
3
3
|
export interface OpenClassifyConfig {
|
|
4
4
|
readonly runner?: OllamaRunnerConfig;
|
|
5
5
|
readonly catalog?: string;
|
|
@@ -7,7 +7,7 @@ export interface OpenClassifyConfig {
|
|
|
7
7
|
}
|
|
8
8
|
export interface OpenClassifyClassifierConfig {
|
|
9
9
|
readonly dirs?: ReadonlyArray<string>;
|
|
10
|
-
readonly stock?:
|
|
10
|
+
readonly stock?: ReadonlyArray<string>;
|
|
11
11
|
}
|
|
12
12
|
export interface OllamaRunnerConfig {
|
|
13
13
|
readonly provider: "ollama";
|
package/dist/src/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { STOCK_CLASSIFIER_NAMES } from "./classifiers.js";
|
|
3
3
|
import { isRecord } from "./validation.js";
|
|
4
|
-
export const DEFAULT_OPEN_CLASSIFY_CONFIG_PATH = "open-classify
|
|
4
|
+
export const DEFAULT_OPEN_CLASSIFY_CONFIG_PATH = "open-classify/config.json";
|
|
5
5
|
export class OpenClassifyConfigError extends Error {
|
|
6
6
|
constructor(message) {
|
|
7
7
|
super(message);
|
|
@@ -30,9 +30,7 @@ export function classifierDirsFromConfig(config) {
|
|
|
30
30
|
return config?.classifiers?.dirs ?? [];
|
|
31
31
|
}
|
|
32
32
|
export function stockClassifierNamesFromConfig(config) {
|
|
33
|
-
return
|
|
34
|
-
.filter(([, enabled]) => enabled)
|
|
35
|
-
.map(([name]) => name);
|
|
33
|
+
return config?.classifiers?.stock ?? [];
|
|
36
34
|
}
|
|
37
35
|
export function validateOpenClassifyConfig(value, path = "open-classify config") {
|
|
38
36
|
if (!isRecord(value)) {
|
|
@@ -56,7 +54,7 @@ function validateClassifiers(value, path) {
|
|
|
56
54
|
...(value.dirs === undefined ? {} : { dirs: validateStringArray(value.dirs, path, "classifiers.dirs") }),
|
|
57
55
|
...(value.stock === undefined
|
|
58
56
|
? {}
|
|
59
|
-
: { stock:
|
|
57
|
+
: { stock: validateEnumArray(value.stock, path, "classifiers.stock", STOCK_CLASSIFIER_NAMES) }),
|
|
60
58
|
};
|
|
61
59
|
}
|
|
62
60
|
function validateRunner(value, path) {
|
|
@@ -116,20 +114,24 @@ function validateStringArray(value, path, field) {
|
|
|
116
114
|
}
|
|
117
115
|
return value.map((item, index) => requireString(item, path, `${field}[${index}]`));
|
|
118
116
|
}
|
|
119
|
-
function
|
|
120
|
-
if (!
|
|
121
|
-
throwConfig(path, `${field} must be an
|
|
117
|
+
function validateEnumArray(value, path, field, allowedValues) {
|
|
118
|
+
if (!Array.isArray(value)) {
|
|
119
|
+
throwConfig(path, `${field} must be an array`);
|
|
122
120
|
}
|
|
123
|
-
const allowed =
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
const allowed = new Set(allowedValues);
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
const out = [];
|
|
124
|
+
for (let i = 0; i < value.length; i++) {
|
|
125
|
+
const item = value[i];
|
|
126
|
+
const name = requireString(item, path, `${field}[${i}]`);
|
|
127
|
+
if (!allowed.has(name)) {
|
|
128
|
+
throwConfig(path, `${field}[${i}] "${name}" is not supported (available: ${[...allowed].join(", ")})`);
|
|
128
129
|
}
|
|
129
|
-
if (
|
|
130
|
-
throwConfig(path, `${field}
|
|
130
|
+
if (seen.has(name)) {
|
|
131
|
+
throwConfig(path, `${field}[${i}] "${name}" is listed more than once`);
|
|
131
132
|
}
|
|
132
|
-
|
|
133
|
+
seen.add(name);
|
|
134
|
+
out.push(name);
|
|
133
135
|
}
|
|
134
136
|
return out;
|
|
135
137
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Adding a classifier
|
|
2
2
|
|
|
3
|
-
Every classifier uses the same two-file layout. Drop a folder into a directory listed under `classifiers.dirs` in `open-classify
|
|
3
|
+
Every classifier uses the same two-file layout. Drop a folder into a directory listed under `classifiers.dirs` in `open-classify/config.json` (defaults to `open-classify/classifiers/` after `npx open-classify init`) and the runtime picks it up on the next start.
|
|
4
4
|
|
|
5
5
|
## 1. Create the directory
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
classifiers/<name>/
|
|
8
|
+
open-classify/classifiers/<name>/
|
|
9
9
|
├── manifest.json
|
|
10
10
|
└── prompt.md
|
|
11
11
|
```
|
|
@@ -71,7 +71,7 @@ Rules:
|
|
|
71
71
|
- `reason` and `certainty` are added to the composed schema by the runtime — don't declare them.
|
|
72
72
|
- `fallback` must validate against the composed schema. Only `reason` and `certainty` are required in fallback; reserved fields and `output_schema.required` fields are exempt (a "no signal" fallback usually omits them).
|
|
73
73
|
- `output_schema.examples` (JSON Schema standard) must validate against the composed schema at load time, so a broken example fails the build, not the model call.
|
|
74
|
-
- **Name collisions throw.**
|
|
74
|
+
- **Name collisions throw.** A user classifier cannot override a mandatory built-in (`preflight`, `model_tier`, `model_specialization`, `prompt_injection`). To customize one of those, use a custom `RunClassifier` to intercept it (see "Replacing the backend" below).
|
|
75
75
|
|
|
76
76
|
See [manifests.md](manifests.md) for the full field list.
|
|
77
77
|
|
|
@@ -87,11 +87,11 @@ Return an empty array when no clear topic applies.
|
|
|
87
87
|
Do not invent tags for vague or ambiguous messages.
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
Don't paste enum values for reserved fields — the runtime injects them with canonical wording so they never drift from
|
|
90
|
+
Don't paste enum values for reserved fields — the runtime injects them with canonical wording so they never drift from the source enums.
|
|
91
91
|
|
|
92
92
|
## 4. Use it
|
|
93
93
|
|
|
94
|
-
After `npx open-classify init`,
|
|
94
|
+
After `npx open-classify init`, `open-classify/classifiers/` exists and `open-classify/config.json` points at it. Drop your folder there and call `createClassifier()`:
|
|
95
95
|
|
|
96
96
|
```ts
|
|
97
97
|
import { createClassifier } from "open-classify";
|
|
@@ -105,31 +105,32 @@ const result = await classify({
|
|
|
105
105
|
const tags = result.classifier_outputs.topic_tags?.tags ?? [];
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
`classifiers.dirs` entries resolve relative to the config file, so the scaffold keeps working even
|
|
108
|
+
`classifiers.dirs` entries resolve relative to the config file, so the scaffold keeps working even when your server starts from a different working directory.
|
|
109
109
|
|
|
110
110
|
If the manifest is malformed, `createClassifier` throws `ClassifierManifestError` at startup with the path and a specific reason — typos fail loud.
|
|
111
111
|
|
|
112
112
|
## Enabling or customizing optional stock classifiers
|
|
113
113
|
|
|
114
|
-
`tools`, `memory_retrieval_queries`, `conversation_digest`, and `context_shift` ship as package-owned optional stock classifiers.
|
|
114
|
+
`tools`, `memory_retrieval_queries`, `conversation_digest`, and `context_shift` ship as package-owned optional stock classifiers. They're off by default. To enable one, list it in `open-classify/config.json`:
|
|
115
115
|
|
|
116
116
|
```json
|
|
117
117
|
{
|
|
118
118
|
"classifiers": {
|
|
119
|
-
"
|
|
120
|
-
|
|
121
|
-
}
|
|
119
|
+
"dirs": ["classifiers"],
|
|
120
|
+
"stock": ["tools"]
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
123
|
```
|
|
125
124
|
|
|
126
|
-
|
|
125
|
+
The package-owned prompt is used, and `npm update open-classify` keeps it current.
|
|
126
|
+
|
|
127
|
+
When you want to take a stock classifier over and edit it:
|
|
127
128
|
|
|
128
129
|
```sh
|
|
129
|
-
|
|
130
|
+
npx open-classify eject tools
|
|
130
131
|
```
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
That copies the stock files into `open-classify/classifiers/tools/`. The runtime transparently switches to your local copy (no config change needed; a local classifier with the same name as a stock classifier always wins). `npm update` won't touch the files. To revert, delete the folder.
|
|
133
134
|
|
|
134
135
|
## Targeting the assistant response
|
|
135
136
|
|
|
@@ -156,7 +157,7 @@ The built-in `prompt_injection` ships tagged `"both"` so it runs on both sides.
|
|
|
156
157
|
|
|
157
158
|
## Choosing the classifier model
|
|
158
159
|
|
|
159
|
-
In `open-classify
|
|
160
|
+
In `open-classify/config.json`:
|
|
160
161
|
|
|
161
162
|
```json
|
|
162
163
|
{
|
package/docs/manifests.md
CHANGED
|
@@ -9,7 +9,7 @@ classifiers/
|
|
|
9
9
|
prompt.md
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
Folders whose names start with `_` are skipped by the loader —
|
|
12
|
+
Folders whose names start with `_` are skipped by the loader — handy if you want to deactivate a classifier without deleting it.
|
|
13
13
|
|
|
14
14
|
## Fields
|
|
15
15
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-classify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Manifest-driven classifier runtime for routing user messages to downstream AI models",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Taylor Bayouth",
|
|
@@ -36,9 +36,8 @@
|
|
|
36
36
|
"bin",
|
|
37
37
|
"dist/src",
|
|
38
38
|
"docs",
|
|
39
|
-
"downstream-models.json",
|
|
40
|
-
"open-classify.config.example.json",
|
|
41
39
|
"templates",
|
|
40
|
+
"CHANGELOG.md",
|
|
42
41
|
"LICENSE",
|
|
43
42
|
"README.md"
|
|
44
43
|
],
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# open-classify/
|
|
2
|
+
|
|
3
|
+
Everything Open Classify reads at runtime lives in this folder:
|
|
4
|
+
|
|
5
|
+
- `config.json` — runtime configuration (Ollama host, model, classifier dirs)
|
|
6
|
+
- `downstream-models.json` — catalog of models the aggregator can route to
|
|
7
|
+
- `classifiers/` — your own classifiers, plus any stock classifiers you've
|
|
8
|
+
ejected for customization
|
|
9
|
+
|
|
10
|
+
To remove Open Classify entirely:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
rm -rf open-classify/
|
|
14
|
+
npm uninstall open-classify
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Stock classifiers
|
|
18
|
+
|
|
19
|
+
Open Classify ships four optional stock classifiers (`tools`,
|
|
20
|
+
`memory_retrieval_queries`, `conversation_digest`, `context_shift`) that
|
|
21
|
+
live inside the `open-classify` package. Enable one by listing its name
|
|
22
|
+
in `config.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"classifiers": {
|
|
27
|
+
"dirs": ["classifiers"],
|
|
28
|
+
"stock": ["tools"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The package-owned prompt is used, and `npm update open-classify` keeps it
|
|
34
|
+
current. When you need to take a stock classifier over and edit it:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npx open-classify eject tools
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
That copies the stock files into `classifiers/tools/`. From that point on,
|
|
41
|
+
the runtime uses your local copy and `npm update` leaves it alone. A local
|
|
42
|
+
classifier always wins on name, so eject works whether or not `tools` is
|
|
43
|
+
listed in `classifiers.stock`. Delete the folder to revert.
|
|
44
|
+
|
|
45
|
+
See the [author guide](https://github.com/taylorbayouth/open-classify/blob/main/docs/adding-a-classifier.md)
|
|
46
|
+
for writing your own classifier from scratch.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# classifiers/
|
|
2
|
+
|
|
3
|
+
Drop a folder here per classifier. Each folder needs two files:
|
|
4
|
+
|
|
5
|
+
- `manifest.json` — declares the output shape and a fallback
|
|
6
|
+
- `prompt.md` — the classification instructions
|
|
7
|
+
|
|
8
|
+
The folder name must match the manifest's `name` field. The runtime picks
|
|
9
|
+
up every classifier here on the next start.
|
|
10
|
+
|
|
11
|
+
To customize one of the four stock classifiers (`tools`,
|
|
12
|
+
`memory_retrieval_queries`, `conversation_digest`, `context_shift`):
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npx open-classify eject tools
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That copies the stock files into `classifiers/tools/`. You own them from
|
|
19
|
+
then on — `npm update open-classify` won't touch them.
|
|
20
|
+
|
|
21
|
+
See the [author guide](https://github.com/taylorbayouth/open-classify/blob/main/docs/adding-a-classifier.md)
|
|
22
|
+
for the full manifest reference.
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"runner": {
|
|
3
|
-
"provider": "ollama",
|
|
4
|
-
"host": "http://127.0.0.1:11434",
|
|
5
|
-
"defaultModel": "gemma4:e4b-it-q4_K_M",
|
|
6
|
-
"options": {
|
|
7
|
-
"temperature": 0,
|
|
8
|
-
"top_p": 1,
|
|
9
|
-
"seed": 0,
|
|
10
|
-
"num_ctx": 4096
|
|
11
|
-
},
|
|
12
|
-
"models": {
|
|
13
|
-
"prompt_injection": "llama-guard3:8b"
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
"catalog": "downstream-models.json",
|
|
17
|
-
"classifiers": {
|
|
18
|
-
"dirs": ["classifiers"],
|
|
19
|
-
"stock": {
|
|
20
|
-
"tools": false,
|
|
21
|
-
"memory_retrieval_queries": false,
|
|
22
|
-
"conversation_digest": false,
|
|
23
|
-
"context_shift": false
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/{memory_retrieval_queries → stock/memory_retrieval_queries}/manifest.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|