Chat engines¶
The chat-engine registry (spec_v3 §4;
ADR-011): an engine decides
what string the retriever searches with, given the turn and its
conversation history, returning the PreparedQuery two-string split —
search_query drives retrieval while original_query (always the user's
words) drives generation. Registered engines: simple (the stateless
identity split) and condense_context (history-resolved standalone
queries; the shipped default stays simple — benchmark-decided).
varagity.chat.base
¶
Chat-engine protocol and registry (spec_v3 §4.2, the spec §5.1 pattern).
Each chat-engine module defines one implementation decorated with
@register("name"); callers resolve the configured engine with
get_chat_engine(settings.CHAT_ENGINE). Registered: simple (the
stateless v2 behavior, verbatim) and condense_context (spec_v3 §4.5 —
follow-ups rewritten into standalone search queries against the
conversation history); adding an engine later means one new file plus its
import line — no caller edits, exactly as the retrieval registry's
reranked addition proved.
An engine decides what string the retriever searches with, given the
turn and its conversation history. It is deliberately not a
:class:~varagity.retrieval.base.Retriever: condensing needs chat history,
which doesn't fit Retriever.retrieve(query, k, …) — widening that
signature would force a parameter on three retrievers that will never use
it.
ChatEngine
¶
Bases: Protocol
Decides what string the retriever searches with, given a turn and its history.
runtime_checkable for the same reason
:class:~varagity.retrieval.base.Retriever is: the protocol appears in
Prefect flow signatures (varagity.pipeline.query_flow), and Prefect
builds a pydantic parameter schema from the annotations at decoration
time, which requires types usable with isinstance.
Source code in varagity/chat/base.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
prepare(query, *, history, llm, verbose)
¶
Prepare the retrieval query for one chat turn.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The user's question, verbatim. |
required |
history
|
Sequence[Turn]
|
Prior turns, oldest first (empty on a first turn). |
required |
llm
|
LLMClient | None
|
Chat client for engines that condense; |
required |
verbose
|
int
|
Validated console verbosity (0–2). |
required |
Returns:
| Type | Description |
|---|---|
PreparedQuery
|
The two-string split downstream stages retrieve and answer with. |
Source code in varagity/chat/base.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
PreparedQuery
dataclass
¶
A chat engine's decision about what to search with (spec_v3 §4.2).
The two-string split is the design's core invariant: the retriever gets
search_query while the answer prompt always gets original_query
— the user's own words and emphasis are never rewritten for generation.
Attributes:
| Name | Type | Description |
|---|---|---|
search_query |
str
|
What the retriever searches with (drives both the query embedding and BM25). |
original_query |
str
|
What the answer prompt gets — always the user's words, verbatim. |
condensed |
bool
|
|
condense_latency_s |
float | None
|
Wall-clock seconds the condense LLM call took,
or |
Source code in varagity/chat/base.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | |
Turn
dataclass
¶
One prior conversation turn, as chat engines consume it.
Attributes:
| Name | Type | Description |
|---|---|---|
role |
str
|
|
content |
str
|
The turn's text (the question or the generated answer). |
Source code in varagity/chat/base.py
27 28 29 30 31 32 33 34 35 36 37 | |
get_chat_engine(name)
¶
Look up a registered chat engine by name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Registry key (e.g. |
required |
Returns:
| Type | Description |
|---|---|
ChatEngine
|
The registered engine instance. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no engine is registered under |
Source code in varagity/chat/base.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | |
register(name)
¶
Class decorator registering a chat engine instance under name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Registry key (the |
required |
Returns:
| Type | Description |
|---|---|
Callable[[T], T]
|
The decorator, which instantiates and registers the class unchanged. |
Source code in varagity/chat/base.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | |
varagity.chat.simple
¶
The simple chat engine: today's stateless behavior, verbatim (spec_v3 §4.2).
SimpleChatEngine
¶
Pass-through engine: search with the user's words, ignore history.
Registering today's behavior as an engine of its own is what proves the
registry isn't a special case built for one implementation — simple
is a peer of every engine that follows it, not the absence of one.
Source code in varagity/chat/simple.py
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
prepare(query, *, history, llm, verbose)
¶
Return the identity split: the query passes through unchanged.
Never calls a model — a simple turn costs no LLM round-trip.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The user's question, verbatim. |
required |
history
|
Sequence[Turn]
|
Prior turns; deliberately ignored. |
required |
llm
|
LLMClient | None
|
Chat client; deliberately ignored. |
required |
verbose
|
int
|
Validated console verbosity; nothing to render. |
required |
Returns:
| Type | Description |
|---|---|
PreparedQuery
|
|
Source code in varagity/chat/simple.py
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
varagity.chat.condense
¶
The condense_context chat engine (spec_v3 §4.5, §4.6).
Rewrites a follow-up question into a standalone search query against the
conversation history — one non-streaming LLM call before retrieval — while
the answer prompt still gets the user's original words (the
:class:~varagity.chat.base.PreparedQuery two-string split).
Failure is a fallback, not an error (§4.6): a transient LLM failure (after
the client's own tenacity retries), an empty result, or an absurdly
long one all degrade to searching with the raw query at WARNING — a
degraded search query still answers a lot of questions; a 500 answers
none. CONDENSE_ENABLED=false is the kill switch, checked inside
:meth:~CondenseContextEngine.prepare exactly as RERANK_ENABLED is
checked inside the reranked retriever — deliberately orthogonal to
engine selection.
CondenseContextEngine
¶
Condense + Context: search with a history-resolved standalone query.
The LlamaIndex "Condense Plus Context" pattern adapted to the registry convention: the condensed rewrite drives retrieval (both the query embedding and BM25) while generation always receives the original question — the user's own words and emphasis are never rewritten for the answer prompt.
Source code in varagity/chat/condense.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | |
prepare(query, *, history, llm, verbose)
¶
Condense the turn against its history into the retrieval query.
The first turn never condenses: empty history means there is
nothing to resolve a reference against, and the common single-turn
question must not pay an LLM round-trip for nothing. The history
fed to the prompt is bounded by CONDENSE_HISTORY_TURNS (newest
turns kept), whatever the caller loaded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The user's question, verbatim. |
required |
history
|
Sequence[Turn]
|
Prior turns, oldest first (empty on a first turn). |
required |
llm
|
LLMClient | None
|
Chat client; resolved via the model registry
( |
required |
verbose
|
int
|
Validated console verbosity (0–2). |
required |
Returns:
| Type | Description |
|---|---|
PreparedQuery
|
The condensed split, or the identity split whenever no LLM |
PreparedQuery
|
call was made (kill switch, first turn) or the call's outcome |
PreparedQuery
|
was unusable (the §4.6 fallback). |
Source code in varagity/chat/condense.py
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |
varagity.chat.prompts
¶
The condense prompt (spec_v3 §4.5) and its history formatting.
A module-level constant following ANSWER_PROMPT
(:mod:varagity.generation.answer) exactly — plain :meth:str.format, no
templating engine — sent as a single user message via the non-streaming
:meth:~varagity.models.llm.LLMClient.generate path.
The topic-shift instruction is the prompt's load-bearing line: dragging the old topic into a question that changed the subject is the failure mode that matters most for a naive condenser (spec_v3 §13.4), and the chat-engine eval fixtures test for it explicitly.
format_history(turns)
¶
Render conversation turns into the prompt's <conversation> block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
turns
|
Sequence[Turn]
|
Prior turns, oldest first. |
required |
Returns:
| Type | Description |
|---|---|
str
|
One |
Source code in varagity/chat/prompts.py
39 40 41 42 43 44 45 46 47 48 | |