|
oc_chat
|
Classes | |
| class | HashEmbedder |
| class | SentenceTransformerEmbedder |
| class | LMStudioEmbedder |
| class | Index |
| class | FitCell |
| class | Fit |
Functions | |
| Any | _numpy () |
| list[dict[str, str]] | read_jsonl (Path path) |
| list[dict[str, str]] | read_cases (Path path) |
| bytes | _npy_header (int rows, int dim) |
| bytes | _flat_float32 (Any matrix, int rows, int dim) |
| Path | write_npy (Path path, Any matrix) |
| tuple[int, int, array] | read_npy (Path path) |
| Any | _normalise_rows (Any matrix) |
| Path | build_index (Path corpus_path, Any embedder, Path index_dir, int|None subset=None, int seed=SUBSET_SEED) |
| Index | load_index (Path index_dir, Any embedder=None) |
| list[float] | index_row (Index index, int position) |
| Any | _similarities (Index index, Sequence[float] query) |
| list[tuple[float, str]] | top_neighbours (Index index, Sequence[float] query, int k) |
| dict[str, float] | class_sums (Iterable[tuple[float, str]] neighbours) |
| dict[str, float] | probabilities (dict[str, float] sums, float temperature, bool mask_absent=True) |
| tuple[str, bool] | verdict_of (dict[str, float] probs) |
| list[float] | embed_query (Index index, str command) |
| dict[str, float] | classify (str command, Index index, int k=DEFAULT_K, float temperature=DEFAULT_TEMPERATURE, bool mask_absent=True) |
| list[list[tuple[float, str]]] | _neighbour_batches (Index index, Any vectors, int k) |
| list[float] | _normalise_one (Sequence[float] vector) |
| Fit | fit (Path validation_path, Index index, Sequence[int] k_grid=DEFAULT_K_GRID, Sequence[float] temperature_grid=DEFAULT_TEMPERATURE_GRID, Sequence[bool] maskings=(True, False)) |
| dict[str, Any] | evaluate (Path cases_path, Index index, int k=DEFAULT_K, float temperature=DEFAULT_TEMPERATURE, bool mask_absent=True, str model_name="knn", Fit|None fitted=None) |
| Path | write_result (dict[str, Any] payload, Path out_path) |
| str | render_fit_table (Fit fitted, bool|None mask_absent=None) |
| str | render_masking_table (Fit fitted) |
| Any | _make_embedder (str kind) |
| argparse.ArgumentParser | _build_parser () |
| str | _leg_tag (argparse.Namespace args) |
| int | main (list[str]|None argv=None) |
Variables | |
| tuple | LABELS = ("allow", "confirm", "deny") |
| str | TIE_BREAK = "deny" |
| tuple | DEFAULT_K_GRID = (1, 5, 15, 51) |
| tuple | DEFAULT_TEMPERATURE_GRID |
| int | DEFAULT_K = 15 |
| float | DEFAULT_TEMPERATURE = 0.1 |
| int | SUBSET_ROWS = 1000 |
| int | SUBSET_SEED = 289 |
| str | JINA_MODEL_ID = "jinaai/jina-embeddings-v2-base-code" |
| int | MAX_SEQ_LENGTH = 512 |
| int | THREADS = 8 |
| str | LMSTUDIO_URL = "http://127.0.0.1:1234/v1/embeddings" |
| str | LMSTUDIO_MODEL = "text-embedding-qwen3-embedding-0.6b" |
| float | LMSTUDIO_TIMEOUT = 30.0 |
| int | BUILD_BATCH = 128 |
| int | FIT_BATCH = 256 |
| str | NPY_MAGIC = b"\x93NUMPY" |
| int | NPY_ALIGNMENT = 64 |
| int | NPY_PREAMBLE = 10 |
| tuple | HASH_NGRAM_SIZES = (1, 2, 3, 4) |
| Any | NUMPY = _numpy() |
Embed the gate-labelled corpus once, then vote with the nearest neighbours.
Purpose
-------
This is the cheapest shape on this bench that learns the argv policy from
labels rather than from prose, and the only one that needs no training loop at
all: embed every corpus command once, embed the query, and read the verdict off
the nearest neighbours' labels. A decision costs one encoder forward pass and
one matrix-vector product, nothing per call, and the whole model is rebuilt by
re-embedding when the gate changes.
It exists to bound plan 290 from below. An encoder that is fine-tuned on this
corpus has to beat what the same encoder already achieves with no training at
all, or the training was not worth its cost; and both have to beat plan 288's
standard-library token floor, which decides in 0.07 ms.
The vote is a softmax over PER-CLASS SIMILARITY SUMS across the top-k cosine
neighbours, divided by a temperature. Two readings of that formula differ in
what happens to a class no neighbour voted for. This module's default masks it
out -- an empty sum is no forecast, not a zero one -- because the alternative
gives a class with no votes ``exp(0)`` of the mass, which at k=1 makes the
nearest-neighbour forecast 1/(1+2exp(-1/T)) rather than the exact 1.0 that a
1-NN forecast actually is. Both readings are implemented and both are swept by
:func:`fit`, so the choice is measured on validation Brier rather than asserted
(see ``mask_absent``).
Ties break toward ``deny``. A vote that cannot separate two classes grants the
least permission, not the most, which matters because the query that produces
an exactly tied vote is the degenerate one -- a command whose embedding is the
zero vector, so every similarity is 0.0 and every present class sums to 0.0.
Run the index build and the evaluation under the SCRATCH HARNESS venv (torch
and sentence-transformers live there, and this bench adds no dependency to any
``pyproject.toml``), from the oc_chat subproject root::
python bench/argv_policy/baselines/knn_classifier.py --stage all
Run the tests under the PROJECT venv, where :class:`HashEmbedder` stands in for
the real encoder so the witnesses need no model, no GPU and no network.
Responsibilities
----------------
- :class:`HashEmbedder` / :class:`SentenceTransformerEmbedder` /
:class:`LMStudioEmbedder` -- the three embedding routes.
- :func:`write_npy` / :func:`read_npy` -- a standard-library ``.npy`` codec, so
an index written here is readable with or without numpy.
- :func:`build_index` / :func:`load_index` -- the index on disk.
- :func:`top_neighbours` / :func:`class_sums` / :func:`probabilities` /
:func:`verdict_of` / :func:`classify` -- one decision.
- :func:`fit` -- k and the temperature from validation Brier alone.
- :func:`evaluate` / :func:`write_result` -- a case set to a plan-288 result
file.
- :func:`main` -- the CLI: build, fit, evaluate.
Diagnostics
-----------
Domain: OC_CHAT.MAIN
Levels:
L2 - index and evaluation lifecycle (build start and finish, files written)
L3 - per-batch, per-fit-cell and per-case-set detail
L4 - module load
Contracts
---------
- Deterministic given an embedder: the corpus, the pinned subset seed and the
(k, temperature) pair fix every number this module reports.
- Read-only with respect to the corpus, the case sets and the scorer.
- Index arrays are megabytes and rebuildable, so :func:`build_index` writes a
``.gitignore`` of ``*`` into its own output directory. Nothing under it is
ever committed, and no model weights are either.
- numpy is used when it is importable and a pure-Python path is used when it is
not; the on-disk format and every number are identical either way. The
fallback is for the witnesses, not for a 27000-row index.
- Console output is ASCII only; commands are printed through :func:`ascii`.
- Standard library only at import time. torch, sentence-transformers and numpy
are imported lazily, inside the routes that need them.
Dependencies
------------
- oc_diagnostics (structured debug logging via _dbg)
- numpy (lazy; fast matrix maths and .npy loading when importable, with a pure-Python fallback)
- sentence_transformers (lazy; the SentenceTransformer encoder behind the required embedding route)
- torch (lazy; pins the encoder's CPU thread count)
|
protected |
Construct the CLI argument parser.
Definition at line 979 of file knn_classifier.py.
|
protected |
Flatten *matrix* to little-endian float32 bytes. Two paths for one format: numpy when the caller handed over an array, and :mod:`array` when it handed over lists. The bytes are identical.
Definition at line 354 of file knn_classifier.py.
|
protected |
The short name identifying one embedder-and-corpus-size leg.
Definition at line 1013 of file knn_classifier.py.
|
protected |
Build the embedder the CLI was asked for.
Definition at line 967 of file knn_classifier.py.
|
protected |
Top-*k* neighbours for a batch of query vectors. With numpy this is one matmul per batch rather than one per query, which is the difference between a fit that takes seconds and one that takes an hour.
Definition at line 723 of file knn_classifier.py.
|
protected |
One vector scaled to unit length, or left alone when it is all zero.
Definition at line 749 of file knn_classifier.py.
|
protected |
Scale every row to unit length, leaving an all-zero row alone. Cosine similarity is a dot product only when both sides are unit-norm, so this is done once at build time and never per query. A zero row cannot be normalised and is kept as it is; it produces a similarity of 0.0 against everything, which is the degenerate case the tie rule handles.
Definition at line 438 of file knn_classifier.py.
|
protected |
The padded version-1.0 header describing a C-order float32 matrix.
Definition at line 346 of file knn_classifier.py.
|
protected |
Return numpy when it is importable, else ``None``. The witnesses run in the project venv, which has neither numpy nor torch; the index builds and the bench evaluations run in the scratch harness venv, which has both. One module serves both, so the import is a question rather than a requirement.
Definition at line 180 of file knn_classifier.py.
|
protected |
Cosine similarity of *query* against every indexed row. A dot product, because both sides are unit-norm: the index rows were normalised at build time and the query is normalised by :func:`classify`.
Definition at line 563 of file knn_classifier.py.
| Path knn_classifier.build_index | ( | Path | corpus_path, |
| Any | embedder, | ||
| Path | index_dir, | ||
| int | None | subset = None, | ||
| int | seed = SUBSET_SEED ) |
Embed a corpus split once and write the index to *index_dir*. Three files: ``vectors.npy`` (row-normalised float32), ``index.json`` (the commands, the labels and how the index was built) and a ``.gitignore`` of ``*``, because the array is megabytes of rebuildable floats and must not be committable by accident.
Definition at line 459 of file knn_classifier.py.
| dict[str, float] knn_classifier.class_sums | ( | Iterable[tuple[float, str]] | neighbours | ) |
Sum the similarities of the neighbours belonging to each class. A class absent from the result is a class no neighbour voted for, which is not the same thing as a class whose votes summed to zero. Membership, not the sign of the sum, is what :func:`probabilities` masks on.
Definition at line 602 of file knn_classifier.py.
| dict[str, float] knn_classifier.classify | ( | str | command, |
| Index | index, | ||
| int | k = DEFAULT_K, | ||
| float | temperature = DEFAULT_TEMPERATURE, | ||
| bool | mask_absent = True ) |
The forecast for one command: embed, search, vote. This is the whole decision, and it is what the latency column measures: one encoder forward pass at batch size one plus one matrix-vector product, with no file read and no model load inside it.
Definition at line 669 of file knn_classifier.py.
| list[float] knn_classifier.embed_query | ( | Index | index, |
| str | command ) |
Embed one command with the index's own encoder, normalised.
Definition at line 656 of file knn_classifier.py.
| dict[str, Any] knn_classifier.evaluate | ( | Path | cases_path, |
| Index | index, | ||
| int | k = DEFAULT_K, | ||
| float | temperature = DEFAULT_TEMPERATURE, | ||
| bool | mask_absent = True, | ||
| str | model_name = "knn", | ||
| Fit | None | fitted = None ) |
Decide every case in *cases_path* and build a plan-288 result payload. P1 asks for a result FILE; producing the payload and writing it are split so the witnesses can validate the schema without touching disk and so one evaluation can be scored twice. :func:`write_result` is the other half. ``condition`` is ``"corpus"`` for every file this module writes (P2): this classifier is never given the policy text, so the policy/nopolicy axis the prompted legs use does not exist for it. The case set therefore lives in *model_name* and in the provenance block, not in the condition column.
Definition at line 824 of file knn_classifier.py.
| Fit knn_classifier.fit | ( | Path | validation_path, |
| Index | index, | ||
| Sequence[int] | k_grid = DEFAULT_K_GRID, | ||
| Sequence[float] | temperature_grid = DEFAULT_TEMPERATURE_GRID, | ||
| Sequence[bool] | maskings = (True, False) ) |
Choose ``k`` and the temperature on the validation split, by Brier. P3: never on a case set. The neighbours are searched ONCE at the largest ``k`` in the grid and every cell reads a prefix of that list, so the sweep costs one pass over the validation split rather than one per cell.
Definition at line 759 of file knn_classifier.py.
| list[float] knn_classifier.index_row | ( | Index | index, |
| int | position ) |
One row of the index as a plain list, whichever backend holds it.
Definition at line 554 of file knn_classifier.py.
| Index knn_classifier.load_index | ( | Path | index_dir, |
| Any | embedder = None ) |
Load an index from disk, optionally with a live embedder for queries.
Definition at line 525 of file knn_classifier.py.
| int knn_classifier.main | ( | list[str] | None | argv = None | ) |
CLI entry point: build the index, fit on validation, score both sets.
Definition at line 1023 of file knn_classifier.py.
| dict[str, float] knn_classifier.probabilities | ( | dict[str, float] | sums, |
| float | temperature, | ||
| bool | mask_absent = True ) |
Softmax the per-class similarity sums over a temperature. With ``mask_absent`` (the default, and the reading in the module docstring) a class no neighbour voted for receives no mass, so a 1-NN forecast is exactly one-hot. Without it, every class enters the softmax with its sum -- zero when absent -- which is the other reading of P1's formula and the arm :func:`fit` sweeps beside the first.
Definition at line 616 of file knn_classifier.py.
| list[dict[str, str]] knn_classifier.read_cases | ( | Path | path | ) |
Read one of the bench's hand-authored case sets.
Definition at line 207 of file knn_classifier.py.
| list[dict[str, str]] knn_classifier.read_jsonl | ( | Path | path | ) |
Read a JSON-lines corpus split into a list of rows.
Definition at line 200 of file knn_classifier.py.
| tuple[int, int, array] knn_classifier.read_npy | ( | Path | path | ) |
Read a float32 ``.npy`` file into ``(rows, dim, flat values)``.
Definition at line 396 of file knn_classifier.py.
| str knn_classifier.render_fit_table | ( | Fit | fitted, |
| bool | None | mask_absent = None ) |
The validation-Brier table W2 requires in the report, as markdown. Pass *mask_absent* to render one arm of the sweep; omit it for the whole grid. :func:`render_masking_table` is the compact comparison of the two.
Definition at line 919 of file knn_classifier.py.
| str knn_classifier.render_masking_table | ( | Fit | fitted | ) |
Best cell per ``k`` under each reading of P1's softmax, as markdown. This is the table that settles the masking question empirically rather than by argument: the same fit rule applied to both arms, side by side.
Definition at line 942 of file knn_classifier.py.
| list[tuple[float, str]] knn_classifier.top_neighbours | ( | Index | index, |
| Sequence[float] | query, | ||
| int | k ) |
The *k* most similar indexed rows as ``(similarity, label)`` pairs.
Definition at line 582 of file knn_classifier.py.
| tuple[str, bool] knn_classifier.verdict_of | ( | dict[str, float] | probs | ) |
The verdict a forecast implies, and whether it came from a tie. Ties break toward :data:`TIE_BREAK`. The caller gets the flag as well as the verdict because plan 289 P3 requires the report to say how often the rule fired: a tie rule nobody counts is a rule nobody can audit.
Definition at line 641 of file knn_classifier.py.
| Path knn_classifier.write_npy | ( | Path | path, |
| Any | matrix ) |
Write *matrix* as a C-order float32 ``.npy`` file. Hand-rolled because the project venv has no numpy and the witnesses must still be able to round-trip an index; the harness venv reads the very same bytes with :func:`numpy.load`.
Definition at line 375 of file knn_classifier.py.
| Path knn_classifier.write_result | ( | dict[str, Any] | payload, |
| Path | out_path ) |
Write one result payload where :mod:`score` can find it.
Definition at line 906 of file knn_classifier.py.
| int knn_classifier.BUILD_BATCH = 128 |
Definition at line 162 of file knn_classifier.py.
| int knn_classifier.DEFAULT_K = 15 |
Definition at line 136 of file knn_classifier.py.
| tuple knn_classifier.DEFAULT_K_GRID = (1, 5, 15, 51) |
Definition at line 122 of file knn_classifier.py.
| float knn_classifier.DEFAULT_TEMPERATURE = 0.1 |
Definition at line 137 of file knn_classifier.py.
| tuple knn_classifier.DEFAULT_TEMPERATURE_GRID |
Definition at line 130 of file knn_classifier.py.
| int knn_classifier.FIT_BATCH = 256 |
Definition at line 166 of file knn_classifier.py.
| tuple knn_classifier.HASH_NGRAM_SIZES = (1, 2, 3, 4) |
Definition at line 177 of file knn_classifier.py.
| str knn_classifier.JINA_MODEL_ID = "jinaai/jina-embeddings-v2-base-code" |
Definition at line 145 of file knn_classifier.py.
| tuple knn_classifier.LABELS = ("allow", "confirm", "deny") |
Definition at line 114 of file knn_classifier.py.
| str knn_classifier.LMSTUDIO_MODEL = "text-embedding-qwen3-embedding-0.6b" |
Definition at line 158 of file knn_classifier.py.
| float knn_classifier.LMSTUDIO_TIMEOUT = 30.0 |
Definition at line 159 of file knn_classifier.py.
| str knn_classifier.LMSTUDIO_URL = "http://127.0.0.1:1234/v1/embeddings" |
Definition at line 157 of file knn_classifier.py.
| int knn_classifier.MAX_SEQ_LENGTH = 512 |
Definition at line 150 of file knn_classifier.py.
| int knn_classifier.NPY_ALIGNMENT = 64 |
Definition at line 172 of file knn_classifier.py.
| str knn_classifier.NPY_MAGIC = b"\x93NUMPY" |
Definition at line 171 of file knn_classifier.py.
| int knn_classifier.NPY_PREAMBLE = 10 |
Definition at line 173 of file knn_classifier.py.
| Any knn_classifier.NUMPY = _numpy() |
Definition at line 197 of file knn_classifier.py.
| int knn_classifier.SUBSET_ROWS = 1000 |
Definition at line 141 of file knn_classifier.py.
| int knn_classifier.SUBSET_SEED = 289 |
Definition at line 142 of file knn_classifier.py.
| int knn_classifier.THREADS = 8 |
Definition at line 154 of file knn_classifier.py.
| str knn_classifier.TIE_BREAK = "deny" |
Definition at line 118 of file knn_classifier.py.