Configuration
The frozen dataclasses you construct and pass in.
Every knob arrives in a frozen dataclass constructed by you, at your own composition root. The library reads no environment variables, here or anywhere: a config object whose values depend on the ambient process is untestable, un-injectable and silently different in production.
Pipeline policy
Tuning knobs for the two pipelines, kept out of the domain models.
These are policy, not data: how big chunks are, whether to contextualise, how many candidates to retrieve before reranking. They live in their own file so the façade signatures (ingest(source, scope, config), query(question, scope, options)) read cleanly and defaults live in exactly one place.
class ragsage.config.IngestionConfig(chunk_size: int = 512, chunk_overlap: int = 64, contextualize: bool = True)
Bases: object
How a document is turned into stored, retrievable chunks.
contextualize toggles Anthropic-style contextual retrieval — prepending
an LLM-written sentence situating each chunk in its document before
embedding. It is on by default (better retrieval) with a per-call off
switch, matching the per-workspace toggle the backend exposes.
chunk_size : int = 512
chunk_overlap : int = 64
contextualize : bool = True
class ragsage.config.QueryOptions(retrieve_k: int = 20, rerank_k: int = 5, context_k: int = 3, min_score: float = 0.0)
Bases: object
How a question is answered: the retrieve -> rerank -> generate funnel.
Defaults follow the spec’s ratio — retrieve a wide candidate set, rerank
with a cross-encoder, and hand only the top few to the model. min_score
is the floor below which retrieval is treated as empty, which is what turns
a weak match into an honest “not found” instead of a hallucination.
retrieve_k : int = 20
rerank_k : int = 5
context_k : int = 3
min_score : float = 0.0
Providers
Provider credentials and per-role model choices, as data the caller owns.
The library never reads os.environ. Every key, model id, and timeout arrives
in this frozen dataclass, constructed by whoever embeds ragsage — a backend
reading its own settings, a CLI reading flags, a test passing literals. That is
the rule ADR-0002 states as “configuration is a frozen dataclass passed by the
caller — never environment variables read at class-definition time”: a library
that reaches for the environment is a library you cannot instantiate twice, test
without monkeypatching, or reason about from the call site.
This config is deliberately small and provider-shaped. Ticket 05 folds it into
one top-level RagSageConfig alongside the storage config; until then it
stands alone so the provider adapters have exactly one place to be configured
from.
type ragsage.providers.config.CallConfig = RunnableConfig
An opaque per-call configuration threaded, untouched, into the underlying LangChain call.
ragsage does not read it, build it, or depend on what is inside it. A consumer
that wants its model calls attributed to a user — the backend binds Langfuse
callbacks this way (its ADR-0003) — constructs one and hands it to the adapter;
ragsage passes it straight through. That observability concern stays entirely
outside this library: there is no Langfuse import anywhere in ragsage, and
tracing proper reaches the engine through the injected Tracer/Span
ports instead. The alias is LangChain’s own type because these adapters are
LangChain clients, so naming it precisely costs nothing and keeps the pass-
through type-checked.
class ragsage.providers.config.ProviderConfig(openai_api_key: str, voyage_api_key: str, embedding_model: str = 'voyage-3-large', rerank_model: str = 'rerank-2', contextualize_model: str = 'gpt-4o-mini', generation_model: str = 'gpt-4o', vision_model: str = 'gpt-4o', timeout_seconds: float = 60.0, context_max_tokens: int = 128, rewrite_max_tokens: int = 256, vision_max_tokens: int = 4096)
Bases: object
Keys, per-role model ids, and call limits for the Voyage/OpenAI adapters.
The roles are split because they are priced and shaped differently, not for
symmetry: embedding and reranking go to Voyage; generation, contextualization,
query rewriting, and vision go to OpenAI, with contextualization and rewriting
sharing a cheap tier. embedding_model is pinned per corpus — changing it
invalidates every stored vector — so it is named here rather than defaulted at
each call site.
An empty API key is permitted and means “no calls to this provider will
succeed”. The provider SDKs reject an empty key at construction, so
build_provider_clients() substitutes a
placeholder: the clients build and the process boots (dev, CI, and any harness
that swaps fakes in at the ports never touches a model), while a real call
fails with an auth error at request time rather than at import time.
openai_api_key : str
voyage_api_key : str
embedding_model : str = 'voyage-3-large'
rerank_model : str = 'rerank-2'
contextualize_model : str = 'gpt-4o-mini'
generation_model : str = 'gpt-4o'
vision_model : str = 'gpt-4o'
timeout_seconds : float = 60.0
context_max_tokens : int = 128
rewrite_max_tokens : int = 256
vision_max_tokens : int = 4096
Postgres
How the caller tells ragsage where its database is — and nothing more.
One frozen dataclass, constructed by the caller and validated on construction.
It reads no environment variables, here or anywhere: ADR-0002 rules that out
explicitly, and the research report singled out the class-definition-time
get_env_value default as the specific anti-pattern to avoid. A config object
whose values depend on the ambient process is untestable, un-injectable, and
silently different in production; a config object whose values came from its
caller is none of those. Wiring env to fields is the consumer’s job, at the
consumer’s composition root.
Everything isolation-related is a field rather than a literal, because ragsage runs inside somebody else’s database. The app role, and the setting name Row-Level Security reads, both belong to whoever is deploying — the defaults here are only ragsage-owned names that will not collide with the consumer’s own.
class ragsage.storage.config.PostgresConfig(dsn: str, app_role: str = 'ragsage_app', isolation_variable: str = 'ragsage.namespace', embedding_dim: int = 1024, text_search_config: str = 'english', pool_size: int = 5, max_overflow: int = 5, pool_pre_ping: bool = True)
Bases: object
Everything ragsage needs to own a connection pool against one database.
- Parameters:
- dsn (str) – The connection URL ragsage connects with. It connects as the owner of
its own table — owners bypass Row-Level Security, which is why every
scoped operation drops into
app_rolefirst (seeopen_scoped_session()). - app_role (str) – The non-privileged Postgres role scoped work runs under. Not a login
identity: it is a privilege context the owner switches into with
SET LOCAL ROLEso RLS is actually enforced. Reaches interpolated SQL, so it is guarded as a plain identifier. - isolation_variable (str) – The run-time setting carrying
Scope.namespacefor the duration of a transaction, and the value ragsage’s RLS policy compares rows against. Configurable because two isolation variables coexist in the deployment this was extracted from — the consumer’sapp.user_idover its own tables, and this one over ragsage’s — and they must not be confused. The default is ragsage-owned for exactly that reason. - embedding_dim (int) – The width of the
embeddingcolumn. Fixed at table-creation time and pinned per corpus — changing it invalidates every stored vector, so it belongs in config rather than as a literal in the DDL. Must match whatever the configured embedder actually returns. - text_search_config (str) – The Postgres text-search configuration the generated
lexicalcolumn is built with ('english'was hardcoded in the consuming backend). It has to be named explicitly because the two-argument form ofto_tsvectoris the IMMUTABLE one, and aSTOREDgenerated column accepts nothing else — the one-argument form depends on a session setting and is only STABLE. Changing it requires rebuilding the column. - pool_size – Pool sizing. Deliberately explicit and deliberately modest: ragsage’s
pool is a second pool reaching the same Postgres as the consumer’s, and
the pair have to fit inside
max_connectionstogether. - max_overflow – Pool sizing. Deliberately explicit and deliberately modest: ragsage’s
pool is a second pool reaching the same Postgres as the consumer’s, and
the pair have to fit inside
max_connectionstogether. - pool_pre_ping (bool) – Check a pooled connection is alive before handing it out. On by default — connections idle across a database restart or a proxy timeout otherwise fail the first query of an unlucky request.
- dsn (str) – The connection URL ragsage connects with. It connects as the owner of
its own table — owners bypass Row-Level Security, which is why every
scoped operation drops into