Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

Alvis

Knowledge ingestion and retrieval engine. Build corporate knowledge bases from heterogeneous sources (GitLab, Confluence, S3; Markdown, HTML, PDF, DOCX, XLSX, CSV) driven by a YAML pipeline — or described from Python with the typed DSL.

Source → Artifact → Extraction → Canonical Content Tree → Chunking → Embedding → Index
                                                Index → nearest-neighbour → SearchHit

Status

1.0.0rc3 (release candidate). Core pipeline runs end-to-end: fs/Confluence/GitHub/GitLab/S3/Notion/Jira/SharePoint/Google Drive/static-URL sources → extract → chunk → embed → memory/SQLite/Qdrant/pgvector/Elasticsearch index, plus retrieval (dense, hybrid, reranked, ACL-filtered — alvis.query, CLI alvis query/alvis chat) and evaluation (alvis eval, retrieval + answer quality). Async-native (httpx). The embedder ships a deterministic placeholder (default) plus an OpenAI-compatible API adapter (openai).

Formats

Extraction (strategy: auto) understands Markdown, HTML, and plain text out of the box, and PDF/DOCX/XLSX when the optional extra is installed:

pip install -e .[documents]

PDF pages become Page N sections, DOCX headings become section headings, and each XLSX worksheet becomes a <sheet title> section with |-joined cells. JSON is parsed structurally: every scalar becomes a section headed by its dotted path (user.address.city), arrays indexed (items.0), so each fact is independently retrievable. CSV yields one Row N section per line, streamed lazily; a text first row is used as a header so rows read as name: Ada | team: core. YAML/TOML are ingested as plain text.

Large binaries are handled safely: parsing runs in a worker thread, XLSX rows stream lazily (read_only), and an optional artifact budget skips oversized files without reading them into memory:

pipeline:
  source:
    type: fs
    config:
      path: ./docs
  extract:
    config:
      max_bytes: 10485760   # skip artifacts > 10 MiB
  index:
    type: memory

The HTTP sources (GitLab/S3/GitHub) stream bodies through the same cap and skip oversize objects instead of buffering them.

Contracts

Both public contracts are versioned and frozen at v1: the YAML schema (pipeline.schema_version, default 1) and the Canonical Content Tree (Document.schema_version). Old configs and trees load unchanged; unknown versions fail fast. The versioning and backward-compatible evolution policy live in docs/schema.md.

Quick start

pip install -e .
alvis --version
alvis init                # scaffold alvis.yaml
alvis validate alvis.yaml              # validation report + pipeline graph
alvis validate alvis.yaml --json       # machine-readable report for CI
alvis run examples/hello-pipeline.yaml   # zero-dependency hello world
alvis run alvis.yaml --dry-run          # describe pipeline without running
alvis status examples/hello-pipeline.yaml  # source health, last run, index count

Secrets are referenced by name (api_token_env, dsn_env, ...) from a .env in the project root (or --env-file); alvis run fails fast listing whatever is missing. See docs/status.md. By default the reference is an environment variable name, resolved through alvis.secrets — swap the backend to HashiCorp Vault (no extra dependency; the KV v2 HTTP API is called directly) by setting ALVIS_SECRETS_BACKEND=vault, ALVIS_VAULT_ADDR, and ALVIS_VAULT_TOKEN; every existing *_env config key then names a <mount>/<path>#<key> Vault reference instead of an env var, with no other config changes. See alvis/secrets.py to implement a custom SecretResolver (AWS Secrets Manager, etc.) and call alvis.secrets. set_resolver(...) before alvis run.

Sources

type what it ingests config keys
fs text files under a directory path, pattern
confluence pages of a space (REST API) url, space, username, api_token_env
github blobs of a repository tree (REST API) repo, branch, path, include_globs, exclude_globs, api_token_env
gitlab blobs of a project repository, or every repo in a group (REST API; self-hosted OK) url, project, group, branch, path, include_globs, exclude_globs, project_include_globs, project_exclude_globs, include_archived, api_token_env
s3 text objects in a bucket (SigV4, no boto3; MinIO-compatible) url, bucket, access_key_env, secret_key_env, region, prefix, include_globs, exclude_globs
static_url plain HTML pages served over HTTP(S), no JS needed urls, api_token_env, max_bytes, timeout
notion pages shared with a Notion integration (REST API) api_token_env
jira issues of a project or JQL query (Jira Cloud REST API) url, project, jql, username, api_token_env
sharepoint files in a site's document library (Microsoft Graph, OAuth2) tenant_id, client_id, site_url, client_secret_env, include_globs, exclude_globs
gdrive files a service account can see (Drive API v3, OAuth2) service_account_key_env, folder_id, include_globs, exclude_globs
none no documents, ever — for query-only pipelines (none)

Every source accepts retries, retry_backoff, and verify. Every remote source (confluence, github, gitlab, s3, static_url, notion, jira, sharepoint, gdrive) also accepts max_concurrency (default 8) — how many document/blob/object/page/issue/file downloads run at once, bounded so a large corpus doesn't hammer the host with one request per document; they all reuse one pooled HTTP client, so raising it is safe up to whatever the remote host can actually take.

notion renders each page's blocks to a lightweight Markdown approximation (headings, lists, quotes, code, paragraphs) — not a faithful reproduction of Notion's richer block types (tables, embeds, synced blocks, databases as tables). Only pages/databases explicitly shared with the integration are visible, by Notion's own access model.

jira matches issues via project (built into project = "KEY" ORDER BY updated DESC) or a raw jql query for anything project can't express; uses the classic /rest/api/2 endpoints, so description comes back as Jira's own wiki markup ingested as plain text, not API v3's Atlassian Document Format. username (account email) + api_token_env is Jira Cloud's Basic auth, same scheme as confluence.

sharepoint authenticates via OAuth2 client-credentials against Azure AD (tenant_id + client_id + client_secret_env — an app registration with an admin-consented Sites.Read.All, or narrower, Microsoft Graph application permission), then walks the site's default document library via Graph's delta query. No Microsoft Graph SDK dependency — plain REST, like every other source here; the one shared-code addition this needed was HttpClient gaining a form request-body option (OAuth2 token exchanges are form-urlencoded, not JSON).

gdrive authenticates with a Google service account: a self-signed RS256 JWT (service_account_key_env — the entire service-account JSON key, client_email + private_key) exchanged for an OAuth2 access token, no user/browser in the loop. Unlike every other source, that signing needs real RSA — the one connector here with a non-optional-at-runtime third-party dependency, pip install alvis[gdrive] (cryptography), rather than the hand-rolled-HTTP approach used elsewhere (S3's SigV4 is HMAC, not RSA — the standard library sufficed there). folder_id scopes to one folder's direct children (not recursive in this version). Google-native documents (Docs/Sheets/Slides/ Forms/Drawings) have no fixed byte content; only Google Docs are exported (as plain text) — everything else under application/vnd.google-apps.* is skipped, not mis-rendered.

none (dsl.none()) has no config and produces zero documents, always. It exists for pipelines built only to query/answer/evaluate an already-ingested index — PipelineConfig.source is required, but those functions never read it. Running a none pipeline (alvis run) is a harmless no-op, not an error, so there's no need for a real (if unused) source just to satisfy the schema — see Embedding in your application below.

Scoping what gets ingested — use prefix/path to restrict a directory or subtree server-side, include_globs to ingest only matching paths, and exclude_globs to skip garbage (videos, logs, vendor dirs); exclude wins over include. With no globs, non-text files are filtered out automatically.

static_url is incremental by default: pages are fingerprinted with their ETag/Last-Modified during a cheap HEAD (falling back to a content hash when a server serves neither), so unchanged pages are never re-downloaded and removed pages are pruned by the per-source reconcile pass.

Example — ingest only docs/** from a bucket, skipping any media:

pipeline:
  source:
    type: s3
    config:
      url: http://localhost:9000
      bucket: alvis
      access_key_env: MINIO_ACCESS_KEY
      secret_key_env: MINIO_SECRET_KEY
      prefix: docs
      include_globs: ["**/*.md", "**/*.rst"]
      exclude_globs: ["**/*.mp4", "**/*.mov"]

End-to-end with docker-compose

Runs the full pipeline against real services (real Qdrant, mock Confluence, MinIO):

docker compose up -d --build
alvis run examples/confluence-qdrant.yaml
alvis run examples/s3-qdrant.yaml        # needs MINIO_ACCESS_KEY / MINIO_SECRET_KEY env vars
# verify: points in the collections
curl http://localhost:6333/collections/alvis_docs
curl http://localhost:6333/collections/alvis-s3
docker compose down          # stop services

The mock Confluence implements the /rest/api contract our adapter consumes (docker/mock-confluence/); swap its url for a real instance when ready. MinIO is seeded (minio-init) with a alvis bucket containing sample docs and a video that should be excluded (examples/s3-seed/).

Several pipelines run in a single command and execute concurrently; a failing pipeline is reported and does not stop the others:

alvis run examples/confluence-qdrant.yaml examples/s3-qdrant.yaml --parallel 2

For a zero-touch monitoring demo (Qdrant + Alvis watch mode + Prometheus + Grafana with a ready-made dashboard), see docker/observability/.

Default pipelines folder

With no paths, alvis run scans alvis/pipelines/*.yaml (plus a top-level alvis.yaml) and runs them concurrently — a project keeps its workflows as plain files in one folder, ready for a mounted container or a scheduler:

alvis/pipelines/
  confluence.yaml
  s3.yaml
alvis run                # runs both, in parallel

Local companion plugins

Plugins normally ship as installable packages. For uninstalled, in-repo code, point --plugins <dir> at a directory of .py files: each file is loaded and expected to assign a module-level plugin (a alvis.plugin.Plugin) or call install_plugin() itself. This runs local code, so it is always explicit — never auto-scanned:

alvis run --plugins ./plugins                 # project-local adapters
alvis validate catalog.yaml --plugins ./plugins
alvis plugins --plugins ./plugins             # list them

Run as a container

Since a pipeline is just YAML, a ready-made image ships the whole product: a alvis CLI with every optional extra (documents parsers, pgvector, observability, gdrive's RSA signing) built in. You configure it by mounting files — never by rebuilding:

docker build -t alvis:dev .
docker run --rm \
  -v "$PWD/pipeline.yaml:/workspace/pipeline.yaml:ro" \
  -v "$PWD/corpus:/workspace/corpus:ro" \
  -v "$PWD/.alvis:/workspace/.alvis" \
  alvis:dev run --incremental pipeline.yaml

Mount a project instead and the defaults kick in: alvis/pipelines/*.yaml is scanned with no args, and local adapters load with --plugins:

docker run --rm -v "$PWD:/workspace" alvis:dev run --plugins ./plugins

CI builds and publishes bzdvdn/alvis (linux/amd64 + linux/arm64) for every v* tag with that tag plus latest — so on release you skip the local build and just pull:

docker pull bzdvdn/alvis:v1.0.0rc3

The image runs as an unprivileged alvis user in /workspace; incremental state lands in .alvis/state.json there, so the volume above is what makes re-runs skip unchanged content. Point a --watch config at a scheduler and the container is the ingestion loop:

docker run -d --name alvis-watcher --restart unless-stopped \
  -v "$PWD/pipeline.yaml:/workspace/pipeline.yaml:ro" \
  -v "$PWD/corpus:/workspace/corpus:ro" \
  -v "$PWD/.alvis:/workspace/.alvis" \
  alvis:dev run --watch --interval 60 pipeline.yaml

Embedding

embed.type: default is a deterministic placeholder for local/dev work. For real vectors use the OpenAI-compatible openai embedder (works with OpenAI, Azure, local servers):

pipeline:
  source:
    type: fs
    config:
      path: ./docs
  embed:
    type: openai
    config:
      base_url: https://api.openai.com/v1
      model: text-embedding-3-small
      api_token_env: OPENAI_API_KEY
      batch_size: 32
  index:
    type: qdrant
    config:
      url: http://localhost:6333
      collection: alvis_docs

Chunks are embedded in batches (batch_size) through the same retry/backoff machinery as sources, and the Qdrant collection is created with whatever dimensionality the model returns. Batches are dispatched concurrently, bounded by max_concurrency (default 4) — a large corpus sends several embedding requests at once instead of one at a time, without unbounded fan-out that could trip the provider's rate limit; set it to 1 for the old fully-sequential behavior.

Embedding cache

Re-running a pipeline re-embeds every chunk unless you enable the cache. cache: true keeps vectors in memory for the process; a dict with path persists them to disk, so unchanged chunks are served from a previous run instead of hitting the embeddings endpoint:

pipeline:
  source:
    type: fs
    config:
      path: ./docs
  embed:
    type: openai
    config:
      base_url: https://api.openai.com/v1
      model: text-embedding-3-small
      api_token_env: OPENAI_API_KEY
      cache:
        path: .alvis/embeddings.cache
  index:
    type: qdrant
    config:
      url: http://localhost:6333
      collection: alvis_docs

Keys are derived from the content hash and the model signature, so switching models never serves stale vectors. Runs report hit/miss counts:

pipeline.yaml: 12 documents, 87 chunks indexed
  embed cache: 85 hits, 2 misses

Chunk strategies

  • auto — paragraph-aware token-budget splitting with overlap (default).
  • sections — one chunk per section heading; oversized sections are split on the budget with the heading repeated as context on continuations.
  • size — pure character-budget splitting (max_chars, overlap_chars) for unstructured sources with no reliable headings.

Indexes

  • qdrant — Qdrant vector store (url, collection). Upserts batch many chunks into one request instead of one per chunk (see alvis.index.base. BatchIndexer), and the HTTP connection pool is reused across the run.
  • pgvector — PostgreSQL + pgvector column (dsn / dsn_env, table, requires pip install alvis[pgindex]). See examples/pgvector.yaml and the postgres service in docker-compose. One connection is opened lazily and reused for the index's lifetime (autocommit) rather than reconnecting per statement.
  • sqlite — persistent single-file store (path), no extra dependencies; a chroma-style dev backend that survives restarts. alvis init --index sqlite.
  • elasticsearch — Elasticsearch's native dense_vector/knn search (url, index; requires Elasticsearch 8.0+, not OpenSearch's separate k-NN plugin dialect — the two forked before their vector-search APIs converged, and this client only speaks Elasticsearch's). No elasticsearch-py dependency — plain REST, like every other backend here. keyword_search (--hybrid) runs Elasticsearch's own BM25 match query server-side, unlike qdrant's local-scoring workaround.
  • memory — in-memory store for tests and prototypes.

All HTTP-backed adapters (qdrant, elasticsearch, every source, the openai embedder, --answer's LLM client) share one pooled httpx.AsyncClient per instance instead of opening a new connection per request; a source/indexer built internally by run/query/eval is closed automatically when the call returns, so long-running --watch loops don't leak connections tick over tick.

Retrieval

Ask questions against an already-loaded index; the query string is embedded with the config's embedder and searched nearest-neighbour:

alvis query examples/pgvector.yaml --text "how do I install alvis?" --top-k 5

--filter key=value (repeatable) restricts candidates to metadata matching every pair — e.g. scope a query to one Confluence space or GitLab project before ranking:

alvis query examples/pgvector.yaml --text "how do I install alvis?" --filter space=ENG

--principal name (repeatable) is the caller's identity for ACL enforcement: excludes chunks whose source declared an acl (see below) that doesn't include any given principal. Omit it for no ACL filtering at all — see ACL-aware retrieval.

--hybrid fuses the dense ranking with a keyword search over the same text (Reciprocal Rank Fusion) — catches exact terms (IDs, acronyms) a dense vector alone can miss. Every built-in index supports it, though the keyword half is scored differently per backend: memory/sqlite BM25 the corpus locally, pgvector ranks via Postgres tsvector/ts_rank, qdrant BM25-scores a server-narrowed full-text candidate pool. A backend without any keyword search (a plugin) falls back to dense-only (logged, not an error):

alvis query examples/sqlite.yaml --text "ERR-4471 retry policy" --hybrid

--rerank reorders the retrieved candidates by relevance via an LLM chat call before truncating to --top-k — no local cross-encoder model needed, it reuses the same OpenAI-compatible chat contract as --answer (--rerank-base-url/--rerank-model/--rerank-api-token-env). Fails soft: a missing API key skips reranking with a warning, and a failed or unparseable model response falls back to the original order — never an error:

alvis query examples/pgvector.yaml --text "how do I install alvis?" --rerank

--answer synthesizes a cited answer over the top hits instead of just returning chunks — works with an OpenAI-compatible endpoint (--answer-base-url/--answer-model/--answer-api-token-env, defaults point at OpenAI), and falls back to numbered excerpts when no API key is set:

alvis query examples/pgvector.yaml --text "how do I install alvis?" --answer

alvis chat is the multi-turn version — an interactive REPL that keeps the conversation's prior question/answer turns in memory and replays them into each answer's synthesis prompt, so a follow-up like "what about the timeout?" is answered with that context. It's a different command, not an --answer flag, because the memory only exists for the life of a running process (there's no session persisted to disk). Retrieval itself still runs on each turn's own text — there is no query-rewriting from history, so a follow-up whose retrieval-relevant terms only exist in an earlier turn may still miss the right chunks even though the answer sounds aware of the conversation:

alvis chat examples/pgvector.yaml --top-k 5
you> how do I install alvis?
alvis> Run `pip install alvis` [1].
  [1] docs/getting-started.md
you> and which Python versions does that need?
alvis> Python 3.10+ [1].
  [1] docs/getting-started.md
you> exit

Same --filter/--principal/--hybrid/--rerank/--answer-* options as query; without --answer-api-token-env set it still works, falling back to numbered excerpts each turn (with no conversation memory in that mode, since there's no model to hand the history to).

Programmatically (same contract as ingestion):

from alvis import query, query_async, answer, answer_async, Synthesizer, LLMReranker, ChatTurn

hits = query("examples/pgvector.yaml", "how do I install alvis?", top_k=5)
await query_async("examples/pgvector.yaml", "how do I install alvis?")
query("examples/pgvector.yaml", "how do I install alvis?", filters={"space": "ENG"})

# LLM-based rerank over the retrieved candidates (fails soft, never raises)
reranker = LLMReranker(base_url="https://api.openai.com/v1", model="gpt-4o-mini",
                       api_token_env="OPENAI_API_KEY")
query("examples/pgvector.yaml", "how do I install alvis?", rerank=reranker)

# cited answer over the hits, with an explicit LLM client
llm = Synthesizer(base_url="https://api.openai.com/v1", model="gpt-4o-mini",
                  api_token_env="OPENAI_API_KEY")
result = answer("examples/pgvector.yaml", "how do I install alvis?", llm=llm)
result.text                      # "Run `pip install alvis` [1], ..."
result.citations                 # [Citation(index=1, source_uri=..., ...)]

# multi-turn: replay prior turns into the next answer's synthesis prompt
history = [ChatTurn(question="how do I install alvis?", answer=result.text)]
followup = answer("examples/pgvector.yaml", "which Python versions?", llm=llm, history=history)

SearchHit carries text, source_uri, metadata, and a cosine score (best first). For in-memory indexes pass the same indexer instance to run/query (persistent backends rebuild their clients from config).

Evaluating retrieval quality

alvis eval scores whether queries actually surface the right document — no LLM required:

alvis run examples/eval-pipeline.yaml
alvis eval examples/eval-pipeline.yaml examples/eval-cases.yaml --min-hit-rate 0.9

Add --judge to also synthesize an answer per case and score it for faithfulness/relevancy via an LLM judge (alvis.judge.AnswerJudge) — a separate axis from retrieval hit rate, the one other opt-in besides --rerank that needs an API key:

alvis eval examples/eval-pipeline.yaml examples/eval-cases.yaml --judge

See docs/evaluation.md for the case format, metrics (hit rate, MRR, faithfulness, relevancy), and the programmatic alvis.evaluate/evaluate_async API.

Embedding in your application

The pipeline is just an async API, so it slots into workers, schedulers, or web apps. alvis exposes thin sync/async entry points:

from alvis import run, run_async, run_many, run_many_async

run("pipeline.yaml")                    # single pipeline, sync (celery/scripts)
await run_async("pipeline.yaml")        # single pipeline, async (FastAPI, ...)

results = run_many(["a.yaml", "b.yaml"], max_parallel=4)   # parallel, sync
await run_many_async(["a.yaml", "b.yaml"])                 # parallel, async

All variants return PipelineResult(documents_ingested, chunks_indexed). Parallel pipelines share one event loop; each pipeline owns its source and index adapters (no shared mutable state). If multiple pipelines write to the same Qdrant collection, give them distinct collections or source identities so the per-source reconcile pass does not prune each other's points.

An app that only ever queries an index another process ingests (a chat backend, say) still needs a PipelineConfig — query/answer/evaluate read its embed/index stages — but never its source. Use dsl.none() rather than a real source you'll never run:

from alvis import dsl, query

query_only_config = dsl.pipeline(
    dsl.none(),
    embed=dsl.embed_openai(base_url="...", model="...", api_token_env="OPENAI_API_KEY"),
    index=dsl.qdrant(url="http://localhost:6333", collection="alvis_docs"),
)
query(query_only_config, "how do I install alvis?")

Describing pipelines in Python

YAML is the declarative interface (no code at all); the typed Python DSL ("no config strings") is a first-class way to describe the exact same pipeline from code, so a Python pipeline validates, dry-runs, and runs identically to its YAML twin:

from alvis import dsl, run

cfg = dsl.pipeline(
    dsl.s3(
        url="http://localhost:9000",
        bucket="alvis",
        access_key_env="MINIO_ACCESS_KEY",
        secret_key_env="MINIO_SECRET_KEY",
        prefix="docs",
        exclude_globs=["**/*.mp4"],
    ),
    chunk=dsl.chunk(max_tokens=80),
    index=dsl.qdrant(url="http://localhost:6333", collection="alvis-s3"),
)
run(cfg)                              # sync, handles its own event loop
await run_async(cfg)                  # or inside your own async app

Stage builders mirror the YAML schema: dsl.fs, dsl.confluence, dsl.github, dsl.gitlab (self-hosted via url), dsl.s3, dsl.extract, dsl.chunk, dsl.embed, dsl.embed_openai, dsl.qdrant, dsl.memory. Defaults are omitted from the underlying config, and pipeline(...) accepts only the stages you want to override. See docs/dsl.md and examples/python_dsl.py.

Re-running a pipeline is idempotent: chunk point IDs are deterministic, so identical content is overwritten, and a reconcile pass prunes points of changed or deleted documents (verified live against Qdrant: 6→6 on re-run).

Payload contract

The Qdrant payload splits into a reserved system namespace and your own metadata. Keys beginning with __ are owned by the engine and drive dedup, incremental skip, and the per-source reconcile pass:

key meaning
__text chunk text (returned by SearchHit.text)
__uri source URI — provenance (returned by SearchHit.source_uri)
__source source identity, namespaces the reconcile prune
__hash content hash, powers skip-unchanged and prune
__document_id stable document identity for citations/per-doc rules
__acl list of principal strings gating visibility (see ACL below)
__schema payload schema version (currently 1)

Everything else is your namespace: metadata is written verbatim and returned in SearchHit.metadata. The engine reserves the __ prefix — metadata keys starting with __ are rejected on write so user data can never overwrite system fields. A metadata key named documentId (or document_id) is promoted to __document_id; sources with a natural stable id declare it (GitLab/GitHub blob id, Confluence page id) so renames update the document in place instead of replacing it. Collections indexed before this contract store the old flat keys — drop the collection once and re-run alvis run to rebuild under the versioned schema.

ACL-aware retrieval

Any source config accepts acl: [principal, ...] — a static list of principal strings (user ids, group names, ...) stamped onto every artifact that source fetches, promoted to __acl at index time:

pipeline:
  source:
    type: fs
    config:
      path: ./internal-docs
      acl: ["eng", "ops"]
  index:
    type: qdrant

alvis query --principal eng (repeatable) then only returns chunks with no acl (public) or an acl that overlaps the given principals — enforced server-side on qdrant/pgvector, in Python on memory/sqlite. Omit --principal entirely and no ACL filtering happens at all — it's opt-in, not a wall you have to work around while testing. Changing a source's acl invalidates --incremental state for that source (it's part of the pipeline signature), so the new principals actually take effect on the next run rather than only on newly-changed documents.

This is a static per-source tag, not live per-document permissions from the origin system (no connector fetches those today) — a future connector can still set a per-chunk acl in extracted metadata, which always wins over the source-level default.

Incremental ingestion

Idempotency avoids duplicates; --incremental also avoids work. Run with --incremental and unchanged documents are skipped end-to-end (no extract / chunk / embed / upsert), reporting the delta:

$ alvis run pipeline.yaml --incremental
pipeline.yaml: 12 documents, 1 chunks indexed
  incremental: 1 changed, 11 skipped, 0 deleted

State lives in .alvis/state.json (--state to relocate) and is keyed by source identity and a pipeline signature — changing extract/chunk/embed settings invalidates it, so chunks are never silently left stale. State is committed only after a successful run, and a failed run leaves the previous state intact. Same toggle programmatically:

run("pipeline.yaml", incremental=True)                    # or state_path="..."
await run_async(cfg, incremental=True, state_path=".alvis/state.json")

Phase 1 skips processing; Phase 2 skips downloading too. Connectors fingerprint documents from listing data alone — S3 ETag (size fallback), GitLab/GitHub blob sha, Confluence version — so unchanged remote objects are never fetched; the filesystem source hashes files locally. A second run of an unchanged 12-document source therefore does zero extraction, chunking, and embedding.

Change-triggered ingestion. Point --watch at the configs and Alvis polls them every --interval seconds, ingesting only what changed per tick — the polling trigger is a scheduler primitive, so a push hook or cron only has to invoke the (already-cheap) incremental run:

$ alvis run pipeline.yaml --watch --interval 60
--watch implies --incremental; enabling incremental mode.
[14:02:11] pipeline.yaml: 12 documents, 0 chunks indexed
  incremental: 0 changed, 12 skipped, 0 deleted

The same loop is available programmatically as an async iterator (alvis.watch_async(configs, interval=..., state_path=...)) — each yielded tick is the per-config PipelineResults, failures included, so a transient source outage doesn't kill the watcher.

Transient failures are retried (429/5xx/connection issues) with exponential backoff and jitter; Retry-After is honored. Per-source/index config accepts retries, retry_backoff, and verify: false for self-signed HTTPS (use the latter only against trusted internal endpoints).

Documentation

License

Apache 2.0. See LICENSE.

Metadata

Release files for alvis 1.0.0rc3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for alvis 1.0.0rc3
File Size Uploaded
alvis-1.0.0rc3.tar.gz 236.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for alvis 1.0.0rc3
File Interpreter ABI Platform
alvis-1.0.0rc3-py3-none-any.whl Python 3 none any Details

Total release size: 404.3 kB

Release files / alvis-1.0.0rc3.tar.gz

Download URL alvis-1.0.0rc3.tar.gz
Size 236.0 kB
Tags Source
SHA-256 checksum
How to use checksums
333f5e62d41fbb48e18a28d1ddc0626c3cbebc4d33418cde65750b006c6bb556
BLAKE2b-256 checksum
How to use checksums
eca80a7aa0586dc424cb68e25636b4aee7aab92eaa02e4cb3a0b0bb98f7db728
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.

Transparency log

Release files / alvis-1.0.0rc3-py3-none-any.whl

Download URL alvis-1.0.0rc3-py3-none-any.whl
Size 168.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f42916b02ac50e9921c06a21393147540f35ca15bdc64cc8d673966804dbb4e9
BLAKE2b-256 checksum
How to use checksums
eb18786cd5d69a5309b70c9d15619e492f4090fa8bd2731c38165ccf7f827372
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page