Modules · Assimilation

The summarizer.

The summarizer is an interposer on the Context Transfer Engine's task interface. It forwards every operation to the pool below it, and after a successful put whose tag and blob names match a configured rule, it sends the blob's bytes to a language model and stores the reply as a sibling blob named with a _label suffix. With no rules configured it is a pure passthrough.

Module
clio_cae_summarizer
Pool ID
401.0 by convention
Backends
Ollama, OpenAI-compatible
Source
context-assimilation-engine/summarizer

What it summarizes

The summarizer watches PutBlob. When a write completes successfully and a rule matches, it reads the written payload, runs a prompt template over it, and stores the model's response as a new blob called {blob_name}_label in the same tag, with the same placement score as the original. Nothing else is summarized: reads, tag creation, metadata operations and searches pass straight through.

Use it when you want a short natural-language description of each ingested document or dataset chunk to exist alongside the data, for example so that keyword search or an agent can find the data by what it says rather than by its name. The summary is written as an ordinary blob, so anything that can read blobs can read it.

Where it sits

The module sits between whatever addresses the CTE and the CTE core itself. Typical chains, from the top down:

  • CAE core as the entry point, then the summarizer, then the CTE core.
  • CAE core, summarizer, indexer, CTE core, as in the dispatch test configuration.
  • A CTE client pointed directly at the summarizer pool.

Its compose key next_pool_id names the pool below. When unset, the canonical CTE core pool is used. The summary blob is written through a client addressed at that lower pool, so the write never re-enters the summarizer. A rule whose blob pattern is .* therefore does not summarize its own summaries.

Like the cache, replication and indexer interposers, the module is deliberately absent from the repository's module registry: regenerating its dispatch code would remove the default-case forwarding it depends on, so its method table is maintained by hand.

Composing it

This is the labeling test configuration. The CTE core comes first because an interposer's next pool must exist when its container is created.

yaml
compose:
  - mod_name: clio_bdev
    pool_name: "ram::chi_default_bdev"
    pool_query: local
    pool_id: "301.0"
    bdev_type: ram
    capacity: "256MB"

  - mod_name: clio_cte_core
    pool_name: cte_core
    pool_query: local
    pool_id: "513.0"
    storage:
      - path: "ram::cte_ram_tier1"
        bdev_type: "ram"
        capacity_limit: "128MB"
        score: 1.0
    dpe:
      dpe_type: "max_bw"

  - mod_name: clio_cae_summarizer
    pool_name: clio_cae_summarizer
    pool_query: local
    pool_id: "401.0"
    next_pool_id: "513.0"
    label_endpoint: "http://127.0.0.1:11434"
    label_prompts:
      summarize: "Summarize the following text in one short sentence."
    label_matches:
      - tag_re: ".*\\.txt$"
        blob_re: ".*"
        model: "gemma3:1b"
        prompt: "summarize"
        context_length: 4096

  - mod_name: clio_cae_core
    pool_name: cae_labeler
    pool_query: local
    pool_id: "512.0"
    next_pool_id: "401.0"

Configuration

next_pool_id
Pool to forward to, as "major.minor". Default: the canonical CTE core pool.
label_endpoint
Base URL of the inference server. The module appends /api/generate for Ollama or /chat/completions for the OpenAI protocol.
label_api
ollama (default when empty) or openai, case-insensitive. Any other spelling fails container creation with an error rather than falling back.
label_api_key_file, label_api_key_env
Where the bearer token for the OpenAI protocol comes from: a file (with ${VAR} expansion) or an environment variable. The file wins when readable. Both are re-read on every request, so a refreshed token needs no restart. Surrounding whitespace is stripped. The token itself is never placed in the compose file. If neither yields a token, requests are sent without an Authorization header and a warning is logged at creation.
label_prompts
A map from prompt name to prompt text. The full model input is the prompt text, a blank line, then the payload chunk.
label_matches
A list of rules, checked in order; the first match wins. Each has tag_re, blob_re, model, prompt (a key in label_prompts), context_length (default 4096) and num_predict (default 0, no cap).

Rule regexes are matched with search semantics, so .*\.txt matches any name ending in .txt without anchors, and globs are not converted. An invalid regex disables only that rule, with a warning, and matching continues with the next. A rule naming a prompt that does not exist is skipped with a warning.

Backends

  • Ollama. A POST to /api/generate with stream: false. context_length is sent as options.num_ctx and num_predict as options.num_predict; a value of 0 omits the option and leaves Ollama's default. The response field is the summary.
  • OpenAI-compatible. A POST to /chat/completions with the prompt as a single user message and stream: false. num_predict is sent as max_tokens when non-zero; context_length is not sent, because these servers fix the context window per model, but it still drives chunking below. The first choice's message content is the summary. This is the protocol hosted inference services speak, and also what a local vLLM or llama.cpp server exposes, usually without a token.

Both calls use libcurl with a 5 second connect timeout and a 120 second total timeout. Loopback addresses bypass any configured proxy, and the environment's no_proxy list is honored in addition. Any transport, HTTP or parse error returns no summary and is logged at warning level.

Chunking

When context_length is positive, the payload is split so each request fits the model's context. Three quarters of the token budget is reserved for the payload and converted to bytes at about 3 bytes per token, less the prompt template's size. Each chunk is summarized separately and the responses are concatenated with a blank line between them. A chunk that fails is skipped; only when every chunk fails is nothing stored. A context_length of 0 disables chunking and sends the whole payload in one request.

Binary payloads tokenize closer to one byte per token, so the byte estimate errs toward splitting more rather than overflowing. With num_predict set, the final summary is roughly that many tokens times the number of chunks.

Operations

  • Create reads the compose keys, resolves the protocol and token source, and logs the number of rules.
  • Destroy clears the tag-name cache.
  • Monitor is the standard no-op status hook.
  • PutBlob forwards first, then summarizes if the write succeeded, the write was not replica-addressed, and a rule matches.
  • Every other CTE core verb (GetBlob, GetOrCreateTag, SemanticSearch, the metadata operations) is forwarded verbatim by the dispatch default.

The summarizer has no client of its own beyond pool creation. Programs talk to it with the ordinary CTE core client, pointed at its pool ID.

Behaviors worth knowing

  • The user's write is never affected. The original put is forwarded before any model call, and its return code is what the caller sees. Every summarization failure is logged and swallowed.
  • Tag names are resolved lazily. A put carries only the tag ID, so the module asks the pool below for the name on the local node and caches it. If the name cannot be resolved on that node, the rule's tag pattern is matched against an empty string.
  • Inference is blocking. The HTTP call runs on the worker that owns the put. A rule that matches a hot write path serializes that path behind the model, so scope tag_re and blob_re narrowly.
  • Summaries inherit placement. The label blob is written with the source blob's score, so it lands on the same kind of tier.
  • The payload is copied. The blob bytes are copied out of shared memory into a private string before the model call, so the request does not share lifetime with the inbound buffer.

Current limits

  • Chunks are summarized one after another on the calling worker; there is no dedicated inference worker pool.
  • Only the two wire protocols above are supported, with one model per rule. There is no local in-process model.
  • Configuration parsing is best-effort: a malformed value leaves the default in place silently, apart from label_api, which fails creation.
  • A put larger than the model's context produces a concatenation of per-chunk summaries, not a single summary of the whole.
  • There is no re-summarization: an overwritten blob gets a new label only because the new put matches again, and a blob written before the rule existed is never labeled.

How it is tested

Tests live under context-assimilation-engine/test/unit. The config-units and rules tests cover compose parsing, rule matching and chunk-boundary arithmetic without a runtime. The label-client and inference tests point the HTTP client at an in-process stub to check both protocols, token resolution, the proxy bypass list and partial-chunk failure. Runtime tests use the compose files in the same directory: the dispatch test against a dead endpoint exercises the match-and-forward path and the SemanticSearch forward, the store test checks the label blob lands in the tag, and the labeling test runs against a local Ollama instance with the configuration shown above.