Tutorial 6: Chat History¶
Chat history lets a node begin its LLM call with prior conversation turns,
making the model aware of context from earlier in the session. Each history
scope is a named list of {role, content} message pairs; a node
references its scope by name via NodePrompt.chat_history.
1. Basic: inline history in YAML¶
Declare history directly in the graph file. Useful for fixed few-shot examples that never change.
models:
- llm: "ollama"
model: "qwen2.5:7b"
host: "http://localhost:11434"
chat_history:
few_shot:
- role: "user"
content: "What is the capital of France?"
- role: "assistant"
content: "Paris."
- role: "user"
content: "And of Germany?"
- role: "assistant"
content: "Berlin."
prompts:
- template:
system_template:
role: You are a geography assistant.
prompt_template:
question: "{user_message}"
nodes:
- id: "assistant"
model: 0
temperature: 0.3
max_tokens: 128
show: true
prompt:
template: 0
user_message: true
chat_history: "few_shot" # injects the two Q&A pairs before the new question
edges:
- node: "assistant"
The model sees the history as genuine prior conversation turns, not as part
of the system prompt. The current user_message follows them.
from kegal import Compiler
with Compiler(uri="few_shot.yml") as compiler:
compiler.user_message = "What is the capital of Italy?"
compiler.compile()
for msg in compiler.get_outputs().nodes[0].response.messages:
print(msg) # "Rome."
2. Intermediate: injecting history at runtime¶
When history is session-specific, manage it in Python and assign it before
each compile() call.
from kegal import Compiler
# Build or load history from your session store
history = [
{"role": "user", "content": "Summarise the project status."},
{"role": "assistant", "content": "The project is on track. Two milestones remain."},
{"role": "user", "content": "When is the next milestone due?"},
{"role": "assistant", "content": "The design review is due on Friday."},
]
with Compiler(uri="assistant.yml") as compiler:
compiler.chat_history = {"session": history}
compiler.user_message = "Which milestone comes after the design review?"
compiler.compile()
compiler.chat_historyis a plain dict — assigning to it overwrites any history loaded from the YAML. To extend rather than replace, read the existing dict first:
3. Intermediate: loading history from a file or URL¶
add_chat_history is a convenience helper that accepts a local JSON file, a
remote https:// URL, or an inline list — exactly one source per call.
from pathlib import Path
from kegal import Compiler
with Compiler(uri="assistant.yml") as compiler:
# from a local JSON file
compiler.add_chat_history("session", file=Path("sessions/user_42.json"))
compiler.user_message = "Continue from where we left off."
compiler.compile()
with Compiler(uri="assistant.yml") as compiler:
# from a remote endpoint (https only)
compiler.add_chat_history(
"session",
uri="https://sessions.example.com/api/history/user_42"
)
compiler.user_message = "What did we discuss last time?"
compiler.compile()
with Compiler(uri="assistant.yml") as compiler:
# from an inline list
compiler.add_chat_history("session", history=[
{"role": "user", "content": "Hello."},
{"role": "assistant", "content": "Hi! How can I help?"},
])
compiler.user_message = "Tell me more."
compiler.compile()
The inline list is copied — subsequent modifications to the original list do not affect the history stored in the compiler.
4. Intermediate: file-based scope in YAML¶
Instead of an inline array, a scope can point to an external JSON file managed on disk. The compiler loads it at construction time.
chat_history:
session_a:
path: ./history/session_a.json # local path relative to the YAML file
auto: false # caller manages persistence (default)
shared_examples:
path: https://example.com/examples.json # https URL — fetched at init
For local paths: if the file does not exist the scope starts empty. For
remote URLs: only https:// is accepted (http:// raises ValueError).
# Load the graph — session_a.json is read at this point
with Compiler(uri="assistant.yml") as compiler:
compiler.user_message = "Continue our discussion."
compiler.compile()
# You must save the updated history manually when auto=false
import json
from pathlib import Path
Path("history/session_a.json").write_text(
json.dumps(compiler.chat_history["session_a"], indent=2)
)
5. Advanced: auto-append mode¶
Set auto: true on a file-based scope to have KeGAL automatically write
the user and assistant turns to the file at the end of every compile().
This turns the graph into a stateful chat session with zero bookkeeping.
chat_history:
session:
path: ./history/session.json
auto: true # KeGAL writes user+assistant turns after every compile()
nodes:
- id: "assistant"
model: 0
temperature: 0.3
max_tokens: 256
show: true
prompt:
template: 0
user_message: true
chat_history: "session"
from kegal import Compiler
# Turn 1
with Compiler(uri="chat.yml") as compiler:
compiler.user_message = "What is the capital of France?"
compiler.compile()
# session.json now contains:
# [{"role": "user", "content": "What is the capital of France?"},
# {"role": "assistant", "content": "Paris."}]
# Turn 2 — new Compiler instance reloads the file
with Compiler(uri="chat.yml") as compiler:
compiler.user_message = "And its population?"
compiler.compile()
# session.json now has all four turns
Each new Compiler instance reloads the file, so the accumulating history
is always available to the model.
Constraints for auto: true:
- Only valid for local file paths — remote URLs cannot be written back.
- The scope must be assigned to at most one node (see §6 below).
6. Advanced: multiple scopes¶
Different nodes in the same graph can reference different history scopes. Each scope is fully independent.
chat_history:
finance_ctx:
- role: "user"
content: "Our Q3 revenue was €4.2 M."
- role: "assistant"
content: "Noted."
legal_ctx:
path: ./history/legal.json
auto: true
nodes:
- id: "finance_analyst"
prompt:
template: 0
chat_history: "finance_ctx"
- id: "legal_analyst"
prompt:
template: 1
chat_history: "legal_ctx"
Scope uniqueness: each scope key may be referenced by at most one node.
Assigning the same scope to two different nodes raises ValueError at
Compiler construction time — this prevents ambiguous auto-append writes.
7. Advanced: chat history in a ReAct controller¶
chat_history on a ReAct controller seeds its conversation buffer. The
controller's iterative LLM calls build on top of this foundation.
chat_history:
research_ctx:
path: ./history/research_ctx.json
auto: true
nodes:
- id: "research_controller"
...
react:
max_iterations: 8
prompt:
template: 0
user_message: true
chat_history: "research_ctx" # seeds the ReAct conversation buffer
The auto-append writes the final user message and final_answer to the file,
so the next run knows what was decided in previous sessions.
Key points¶
chat_historyis a dict of named scopes; each scope is a list of{role, content}pairs.- Inline scopes (array in YAML) are always managed by the caller.
- File-based scopes are loaded at
Compilerconstruction time;auto: truewrites back after eachcompile(). auto: trueis not supported for remote URL scopes.- Each scope may be assigned to at most one node — sharing a scope between
two nodes raises
ValueError. add_chat_history(id, *, file, uri, history)accepts exactly one source.
Related tutorials: 12 ReAct loop — using chat_history to seed a controller's conversation
05 RAG — combining retrieved context with conversation history