Training examples live in labeled datasets. Runtime routing does not accept a train: DSL flag; local classifier artifacts and semantic-cache dataset mirroring are separate systems.

For modern labeled datasets, write rows with text and label/intent and let the classifier trainer consume them directly:

[
  {"text": "how much does it cost?", "label": "PRICING"},
  {"text": "my API key is failing", "intent": "TECHNICAL_SUPPORT"}
]

Classifier artifacts route by label when :classifier can see the route. Semantic cache mirrors labeled dataset rows by default unless the route uses cache: false.

You can configure those sources globally:

config :spectre, :classifier,
  dataset_path: "training/dataset.json",
  artifact_dir: "artifacts/spectre"

Do not put training examples in the DSL. train:, training:, train: [...], train: "file.jsonl", and similar inline training forms are invalid.

Dataset files can be:

  • .json list of objects
  • .jsonl one object per line

Rows can use either label or intent. Labeled .json and .jsonl rows are also mirrored into exact semantic-cache lookup by default for cacheable routes:

[
  {"text": "how much does it cost?", "label": "PRICING"},
  {"text": "my API key is failing", "intent": "TECHNICAL_SUPPORT"}
]

Export rows from an agent:

mix spectre.classifier.dataset MyApp.SupportAgent \
  training/support/dataset.json \
  --source training/raw/support.jsonl

Train a local classifier artifact:

mix spectre.classifier.download_model --model intfloat/multilingual-e5-small
mix spectre.classifier.train training/support/dataset.json priv/spectre/support

Training writes a semantic_cache.jsonl companion into the artifact directory. It contains each training row together with the embedding already created during training. At runtime, Spectre filters it to cacheable routes and loads the saved vectors; it does not call the embedding provider once per dataset row.

Configure the classifier:

config :spectre, :classifier,
  artifact_dir: "priv/spectre/support",
  encoder_model: "intfloat/multilingual-e5-small",
  embedding_adapter: Spectre.Classifier.Embeddings.ExFastembed

Classifier artifacts use compact centroids by default. At runtime, the local classifier indexes centroids with Vettore, while semantic-cache search uses the saved row vectors in semantic_cache.jsonl.

For larger datasets, nearest-example routing can be worth the extra memory:

config :spectre, :classifier,
  local_classifier_mode: :examples,
  local_classifier_index: :hnsw,
  local_classifier_index_options: [ef_search: 64],
  local_example_score: :max

Use :mean for local_example_score if you prefer averaging returned hits per label.

Online learning review

Rows mirrored from an offline labeled dataset are trusted immediately. Rows created at runtime through SemanticCache.put/3 are different: they remain editable but unverified and are excluded from exact and semantic routing until SemanticCache.verify/3 or SemanticCache.relabel/4 approves them.

{:ok, row} =
  Spectre.Router.SemanticCache.put(
    "need a custom quote",
    %{label: :SALES, strategy: :llm_classifier},
    spectre_agent: MyApp.SupportAgent
  )

{:ok, pending_review} =
  Spectre.Router.SemanticCache.examples(MyApp.SupportAgent)

{:ok, verified} =
  Spectre.Router.SemanticCache.verify(MyApp.SupportAgent, row.id)

This prevents an unverified classifier or LLM decision from immediately becoming ground truth. Review APIs still list quarantined rows, and snapshots preserve both their verification state and embedding.

The runtime request invariant is:

  • an accepted exact-cache hit makes zero embedding calls;
  • a non-exact semantic lookup embeds only the incoming text once;
  • a newly learned row stores its embedding for later snapshots; and
  • loading a snapshot never regenerates every stored embedding.

Built-in semantic indexes are bounded to four cached revisions per agent by default. Override this only when an agent intentionally uses several embedding configurations:

config :spectre, :semantic_cache, index_capacity: 8

Use :unlimited explicitly if unbounded index retention is truly intended.