Rename initial_context to background_context

This commit is contained in:
Yiorgis Gozadinos 2026-01-16 14:58:44 +02:00
parent 6c5c23b338
commit bdfc6b87e7
No known key found for this signature in database
17 changed files with 122 additions and 105 deletions

View file

@ -3,13 +3,16 @@
### Added
- **Background Context Support**: Pass initial context to agents via CLI or Python API
- **Background Context Support**: Pass background context to agents via CLI or Python API
- `haiku-rag ask --context "..." --context-file path` for Q&A with background context
- `haiku-rag research --context "..." --context-file path` for research with background context
- `haiku-rag chat --context "..." --context-file path` for chat sessions with persistent context
- `ResearchContext(initial_context="...")` for Python API usage
- `ChatSessionState(initial_context="...")` for chat agent sessions
- `ResearchContext(background_context="...")` for Python API usage
- `ChatSessionState(background_context="...")` for chat agent sessions
- Context is included in agent system prompts and research graph planning
- **Frontend Background Context**: Settings panel in the chat app to configure persistent background context
- Context is stored in localStorage and sent with each conversation
- **Frontend Linting**: Added Biome for linting and formatting the frontend codebase
## [0.26.4] - 2026-01-15

View file

@ -82,7 +82,7 @@ async def stream_chat(request: Request) -> Response:
# Restore session state from incoming AG-UI state (look under namespaced key)
initial_qa_history: list[QAResponse] = []
initial_context: str | None = None
background_context: str | None = None
state = getattr(run_input, "state", None)
if state:
chat_state = state.get(AGUI_STATE_KEY, state)
@ -90,7 +90,7 @@ async def stream_chat(request: Request) -> Response:
initial_qa_history = [
QAResponse(**qa) for qa in chat_state.get("qa_history", [])
]
initial_context = chat_state.get("initial_context")
background_context = chat_state.get("background_context")
# Build deps with session state
thread_id = getattr(run_input, "thread_id", None)
@ -100,7 +100,7 @@ async def stream_chat(request: Request) -> Response:
session_state=ChatSessionState(
session_id=thread_id or "",
qa_history=initial_qa_history,
initial_context=initial_context,
background_context=background_context,
),
state_key=AGUI_STATE_KEY,
)

View file

@ -38,7 +38,7 @@ interface ChatSessionState {
session_id: string;
citations: Citation[];
qa_history: QAResponse[];
initial_context: string | null;
background_context: string | null;
}
// AG-UI state is namespaced under AGUI_STATE_KEY
@ -358,24 +358,24 @@ function ToolCallIndicator({
}
function ChatContentInner({
initialContext,
setInitialContext,
backgroundContext,
setBackgroundContext,
}: {
initialContext: string;
setInitialContext: (value: string) => void;
backgroundContext: string;
setBackgroundContext: (value: string) => void;
}) {
const [settingsOpen, setSettingsOpen] = useState(false);
const handleSaveContext = useCallback(
(value: string) => {
setInitialContext(value);
setBackgroundContext(value);
if (value) {
localStorage.setItem(STORAGE_KEY, value);
} else {
localStorage.removeItem(STORAGE_KEY);
}
},
[setInitialContext],
[setBackgroundContext],
);
useCoAgent<AgentState>({
@ -385,7 +385,7 @@ function ChatContentInner({
session_id: "",
citations: [],
qa_history: [],
initial_context: initialContext || null,
background_context: backgroundContext || null,
},
},
});
@ -517,10 +517,10 @@ function ChatContentInner({
<div className="chat-header">
<button
type="button"
className={`settings-btn ${initialContext ? "has-context" : ""}`}
className={`settings-btn ${backgroundContext ? "has-context" : ""}`}
onClick={() => setSettingsOpen(true)}
title={
initialContext
backgroundContext
? "Background context is set"
: "Set background context"
}
@ -545,28 +545,30 @@ function ChatContentInner({
isOpen={settingsOpen}
onClose={() => setSettingsOpen(false)}
onSave={handleSaveContext}
currentValue={initialContext}
currentValue={backgroundContext}
/>
</>
);
}
function ChatContent() {
const [initialContext, setInitialContext] = useState<string | null>(null);
const [backgroundContext, setBackgroundContext] = useState<string | null>(
null,
);
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY);
setInitialContext(stored || "");
setBackgroundContext(stored || "");
}, []);
if (initialContext === null) {
if (backgroundContext === null) {
return null;
}
return (
<ChatContentInner
initialContext={initialContext}
setInitialContext={setInitialContext}
backgroundContext={backgroundContext}
setBackgroundContext={setBackgroundContext}
/>
);
}

View file

@ -2,12 +2,12 @@
import { useCallback, useEffect, useId, useState } from "react";
const STORAGE_KEY = "haiku.rag.settings.initial_context";
const STORAGE_KEY = "haiku.rag.settings.background_context";
interface SettingsPanelProps {
isOpen: boolean;
onClose: () => void;
onSave: (initialContext: string) => void;
onSave: (backgroundContext: string) => void;
currentValue: string;
}

View file

@ -103,7 +103,7 @@ The `ChatSessionState` maintains:
- `session_id` — Unique identifier for the session
- `qa_history` — List of previous Q/A pairs (FIFO, max 50)
- `initial_context` — Optional background context for the conversation
- `background_context` — Optional background context for the conversation
- `embedding_cache` — Cached embeddings for semantic ranking
Q/A history is used to:
@ -118,7 +118,7 @@ You can provide background context that persists throughout the conversation:
```python
session = ChatSessionState(
initial_context="Focus on Python programming concepts and best practices."
background_context="Focus on Python programming concepts and best practices."
)
deps = ChatDeps(client=client, config=config, session_state=session)
```
@ -235,12 +235,12 @@ async with HaikuRAG(path_to_db) as client:
```python
context = ResearchContext(
original_question="What are the safety protocols?",
initial_context="Industrial manufacturing and workplace safety domain."
background_context="Industrial manufacturing and workplace safety domain."
)
state = ResearchState.from_config(context=context, config=Config)
```
The `initial_context` provides domain background that helps the planning and synthesis agents understand the context of the research question.
The `background_context` provides domain background that helps the planning and synthesis agents understand the context of the research question.
**With custom config:**

View file

@ -33,10 +33,10 @@ def create_chat_agent(config: AppConfig) -> Agent[ChatDeps, str]:
)
@agent.system_prompt
async def add_initial_context(ctx: RunContext[ChatDeps]) -> str:
"""Add initial_context to system prompt when available."""
if ctx.deps.session_state and ctx.deps.session_state.initial_context:
return f"\nBACKGROUND CONTEXT:\n{ctx.deps.session_state.initial_context}"
async def add_background_context(ctx: RunContext[ChatDeps]) -> str:
"""Add background_context to system prompt when available."""
if ctx.deps.session_state and ctx.deps.session_state.background_context:
return f"\nBACKGROUND CONTEXT:\n{ctx.deps.session_state.background_context}"
return ""
@agent.tool
@ -93,8 +93,8 @@ def create_chat_agent(config: AppConfig) -> Agent[ChatDeps, str]:
qa_history=(
ctx.deps.session_state.qa_history if ctx.deps.session_state else []
),
initial_context=(
ctx.deps.session_state.initial_context
background_context=(
ctx.deps.session_state.background_context
if ctx.deps.session_state
else None
),
@ -194,14 +194,16 @@ def create_chat_agent(config: AppConfig) -> Agent[ChatDeps, str]:
# Build and run the conversational research graph
graph = build_conversational_graph(config=ctx.deps.config)
initial_context = (
ctx.deps.session_state.initial_context if ctx.deps.session_state else None
background_context = (
ctx.deps.session_state.background_context
if ctx.deps.session_state
else None
)
context = ResearchContext(
original_question=question,
qa_responses=existing_qa,
initial_context=initial_context,
background_context=background_context,
)
state = ResearchState(
context=context,
@ -255,8 +257,8 @@ def create_chat_agent(config: AppConfig) -> Agent[ChatDeps, str]:
qa_history=(
ctx.deps.session_state.qa_history if ctx.deps.session_state else []
),
initial_context=(
ctx.deps.session_state.initial_context
background_context=(
ctx.deps.session_state.background_context
if ctx.deps.session_state
else None
),

View file

@ -61,7 +61,7 @@ class ChatSessionState(BaseModel):
session_id: str = ""
citations: list[CitationInfo] = []
qa_history: list[QAResponse] = []
initial_context: str | None = None
background_context: str | None = None
def format_conversation_context(qa_history: list[QAResponse]) -> str:
@ -200,8 +200,10 @@ class ChatDeps:
CitationInfo(**c) if isinstance(c, dict) else c
for c in state_data.get("citations", [])
]
if "initial_context" in state_data:
self.session_state.initial_context = state_data.get("initial_context")
if "background_context" in state_data:
self.session_state.background_context = state_data.get(
"background_context"
)
if "session_id" in state_data:
self.session_state.session_id = state_data.get("session_id", "")

View file

@ -19,7 +19,7 @@ class ResearchContext(BaseModel):
qa_responses: list[Any] = Field(
default_factory=list, description="Structured QA pairs used during research"
)
initial_context: str | None = Field(
background_context: str | None = Field(
default=None,
description="Optional background context provided at session start",
)

View file

@ -33,8 +33,8 @@ def format_context_for_prompt(context: ResearchContext) -> str:
"""Format the research context as XML for planning prompts."""
context_data: dict[str, object] = {}
if context.initial_context:
context_data["background"] = context.initial_context
if context.background_context:
context_data["background"] = context.background_context
context_data["question"] = context.original_question
@ -61,8 +61,8 @@ def format_conversational_context_for_prompt(context: ResearchContext) -> str:
"""Format context for synthesis prompts."""
context_data: dict[str, object] = {}
if context.initial_context:
context_data["background"] = context.initial_context
if context.background_context:
context_data["background"] = context.background_context
context_data["question"] = context.original_question
@ -98,7 +98,7 @@ async def _plan_step_logic(
# Use context-aware prompt if we have existing qa_responses
has_prior_answers = bool(state.context.qa_responses)
has_background = bool(state.context.initial_context)
has_background = bool(state.context.background_context)
effective_plan_prompt = (
build_prompt(PLAN_PROMPT_WITH_CONTEXT, config)
if has_prior_answers

View file

@ -376,7 +376,7 @@ class HaikuRAGApp:
cite: bool = False,
deep: bool = False,
filter: str | None = None,
initial_context: str | None = None,
background_context: str | None = None,
):
"""Ask a question using the RAG system.
@ -385,7 +385,7 @@ class HaikuRAGApp:
cite: Include citations in the answer
deep: Use deep QA mode (multi-step reasoning)
filter: SQL WHERE clause to filter documents
initial_context: Optional background context for the question
background_context: Optional background context for the question
"""
async with HaikuRAG(
db_path=self.db_path,
@ -397,7 +397,7 @@ class HaikuRAGApp:
if deep:
graph = build_research_graph(config=self.config)
context = ResearchContext(
original_question=question, initial_context=initial_context
original_question=question, background_context=background_context
)
state = ResearchState.from_config(
context=context,
@ -428,8 +428,8 @@ class HaikuRAGApp:
self.console.print("[yellow]No answer generated.[/yellow]")
else:
system_prompt = (
f"BACKGROUND CONTEXT:\n{initial_context}"
if initial_context
f"BACKGROUND CONTEXT:\n{background_context}"
if background_context
else None
)
answer, citations = await self.client.ask(
@ -448,14 +448,14 @@ class HaikuRAGApp:
self,
question: str,
filter: str | None = None,
initial_context: str | None = None,
background_context: str | None = None,
):
"""Run research via the pydantic-graph pipeline.
Args:
question: The research question
filter: SQL WHERE clause to filter documents
initial_context: Optional background context for the research
background_context: Optional background context for the research
"""
async with HaikuRAG(
db_path=self.db_path,
@ -469,7 +469,7 @@ class HaikuRAGApp:
graph = build_research_graph(config=self.config)
context = ResearchContext(
original_question=question, initial_context=initial_context
original_question=question, background_context=background_context
)
state = ResearchState.from_config(context=context, config=self.config)
state.search_filter = filter

View file

@ -6,7 +6,7 @@ def run_chat(
db_path: Path | None = None,
read_only: bool = False,
before: datetime | None = None,
initial_context: str | None = None,
background_context: str | None = None,
) -> None:
"""Run the chat TUI.
@ -14,7 +14,7 @@ def run_chat(
db_path: Path to the LanceDB database. If None, uses default from config.
read_only: Whether to open the database in read-only mode.
before: Query database as it existed before this datetime.
initial_context: Optional background context for the conversation.
background_context: Optional background context for the conversation.
"""
try:
from haiku.rag.chat.app import ChatApp
@ -30,6 +30,9 @@ def run_chat(
db_path = config.storage.data_dir / "haiku.rag.lancedb"
app = ChatApp(
db_path, read_only=read_only, before=before, initial_context=initial_context
db_path,
read_only=read_only,
before=before,
background_context=background_context,
)
app.run()

View file

@ -89,13 +89,13 @@ class ChatApp(App): # type: ignore[misc]
db_path: Path,
read_only: bool = False,
before: datetime | None = None,
initial_context: str | None = None,
background_context: str | None = None,
) -> None:
super().__init__()
self.db_path = db_path
self.read_only = read_only
self.before = before
self.initial_context = initial_context
self.background_context = background_context
self.client: HaikuRAG | None = None
self.config = get_config()
self.agent: Agent[ChatDeps, str] | None = None
@ -128,7 +128,7 @@ class ChatApp(App): # type: ignore[misc]
self.agent = create_chat_agent(self.config)
self.session_state = ChatSessionState(
session_id=str(uuid.uuid4()),
initial_context=self.initial_context,
background_context=self.background_context,
)
# Focus the input field
@ -274,10 +274,10 @@ class ChatApp(App): # type: ignore[misc]
self._last_citations.clear()
self._selected_citation_idx = None
self._message_history.clear()
# Reset session state for fresh conversation (preserve initial_context)
# Reset session state for fresh conversation (preserve background_context)
self.session_state = ChatSessionState(
session_id=str(uuid.uuid4()),
initial_context=self.initial_context,
background_context=self.background_context,
)
def action_focus_input(self) -> None:

View file

@ -350,11 +350,11 @@ def ask(
),
):
# Resolve initial context from flag or file
initial_context: str | None = None
background_context: str | None = None
if context_file:
initial_context = context_file.read_text()
background_context = context_file.read_text()
elif context:
initial_context = context
background_context = context
app = create_app(db)
asyncio.run(
@ -363,7 +363,7 @@ def ask(
cite=cite,
deep=deep,
filter=filter,
initial_context=initial_context,
background_context=background_context,
)
)
@ -394,15 +394,17 @@ def research(
),
):
# Resolve initial context from flag or file
initial_context: str | None = None
background_context: str | None = None
if context_file:
initial_context = context_file.read_text()
background_context = context_file.read_text()
elif context:
initial_context = context
background_context = context
app = create_app(db)
asyncio.run(
app.research(question=question, filter=filter, initial_context=initial_context)
app.research(
question=question, filter=filter, background_context=background_context
)
)
@ -608,14 +610,17 @@ def chat(
db_path = db if db else get_config().storage.data_dir / "haiku.rag.lancedb"
# Resolve initial context from flag or file
initial_context: str | None = None
background_context: str | None = None
if context_file:
initial_context = context_file.read_text()
background_context = context_file.read_text()
elif context:
initial_context = context
background_context = context
run_chat(
db_path, read_only=_read_only, before=_before, initial_context=initial_context
db_path,
read_only=_read_only,
before=_before,
background_context=background_context,
)

View file

@ -77,14 +77,14 @@ def test_chat_session_state():
def test_chat_agent_has_dynamic_system_prompt():
"""Test that chat agent registers a dynamic system prompt for initial_context."""
"""Test that chat agent registers a dynamic system prompt for background_context."""
agent = create_chat_agent(Config)
# The agent should have at least one system prompt function registered
# (the add_initial_context function)
# (the add_background_context function)
assert len(agent._system_prompt_functions) >= 1
# Verify it's the add_initial_context function
# Verify it's the add_background_context function
func_names = [r.function.__name__ for r in agent._system_prompt_functions]
assert "add_initial_context" in func_names
assert "add_background_context" in func_names
def test_citation_info():

View file

@ -250,23 +250,23 @@ def test_max_qa_history_constant():
assert MAX_QA_HISTORY == 50
def test_chat_session_state_initial_context():
"""Test ChatSessionState accepts initial_context."""
def test_chat_session_state_background_context():
"""Test ChatSessionState accepts background_context."""
from haiku.rag.agents.chat.state import ChatSessionState
state = ChatSessionState(
session_id="test-session",
initial_context="This is background knowledge about the topic.",
background_context="This is background knowledge about the topic.",
)
assert state.initial_context == "This is background knowledge about the topic."
assert state.background_context == "This is background knowledge about the topic."
def test_chat_session_state_initial_context_defaults_to_none():
"""Test ChatSessionState initial_context defaults to None."""
def test_chat_session_state_background_context_defaults_to_none():
"""Test ChatSessionState background_context defaults to None."""
from haiku.rag.agents.chat.state import ChatSessionState
state = ChatSessionState(session_id="test-session")
assert state.initial_context is None
assert state.background_context is None
def test_chat_deps_state_getter_returns_namespaced_state():
@ -283,7 +283,7 @@ def test_chat_deps_state_getter_returns_namespaced_state():
qa_history=[
QAResponse(question="Q1", answer="A1", confidence=0.9),
],
initial_context="Background info",
background_context="Background info",
)
deps = ChatDeps(
@ -299,7 +299,7 @@ def test_chat_deps_state_getter_returns_namespaced_state():
assert state[AGUI_STATE_KEY]["session_id"] == "test-123"
assert len(state[AGUI_STATE_KEY]["qa_history"]) == 1
assert state[AGUI_STATE_KEY]["qa_history"][0]["question"] == "Q1"
assert state[AGUI_STATE_KEY]["initial_context"] == "Background info"
assert state[AGUI_STATE_KEY]["background_context"] == "Background info"
def test_chat_deps_state_getter_without_namespace():
@ -368,7 +368,7 @@ def test_chat_deps_state_setter_updates_from_namespaced_state():
{"question": "Q1", "answer": "A1", "confidence": 0.9, "citations": []}
],
"citations": [],
"initial_context": "New context",
"background_context": "New context",
}
}
@ -378,7 +378,7 @@ def test_chat_deps_state_setter_updates_from_namespaced_state():
assert deps.session_state.session_id == "updated-123"
assert len(deps.session_state.qa_history) == 1
assert deps.session_state.qa_history[0].question == "Q1"
assert deps.session_state.initial_context == "New context"
assert deps.session_state.background_context == "New context"
def test_chat_deps_state_setter_handles_none():
@ -458,7 +458,7 @@ def test_chat_deps_state_setter_with_citation_dicts():
"content": "Test content",
}
],
"initial_context": None,
"background_context": None,
}
}

View file

@ -46,19 +46,19 @@ async def test_graph_end_to_end(allow_model_requests, temp_db_path, qa_corpus):
client.close()
def test_research_context_initial_context():
"""Test ResearchContext accepts initial_context."""
def test_research_context_background_context():
"""Test ResearchContext accepts background_context."""
context = ResearchContext(
original_question="What is X?",
initial_context="Background: X is a concept in domain Y.",
background_context="Background: X is a concept in domain Y.",
)
assert context.initial_context == "Background: X is a concept in domain Y."
assert context.background_context == "Background: X is a concept in domain Y."
def test_research_context_initial_context_defaults_to_none():
"""Test ResearchContext initial_context defaults to None."""
def test_research_context_background_context_defaults_to_none():
"""Test ResearchContext background_context defaults to None."""
context = ResearchContext(original_question="What is X?")
assert context.initial_context is None
assert context.background_context is None
def test_format_context_for_prompt_includes_background():
@ -67,7 +67,7 @@ def test_format_context_for_prompt_includes_background():
context = ResearchContext(
original_question="What is X?",
initial_context="X is a concept in domain Y.",
background_context="X is a concept in domain Y.",
)
result = format_context_for_prompt(context)
assert "X is a concept in domain Y." in result
@ -89,7 +89,7 @@ def test_format_conversational_context_for_prompt_includes_background():
context = ResearchContext(
original_question="What is X?",
initial_context="X is a concept in domain Y.",
background_context="X is a concept in domain Y.",
)
result = format_conversational_context_for_prompt(context)
assert "X is a concept in domain Y." in result

View file

@ -283,7 +283,7 @@ def test_ask():
cite=False,
deep=False,
filter=None,
initial_context=None,
background_context=None,
)
@ -301,7 +301,7 @@ def test_ask_with_cite():
cite=True,
deep=False,
filter=None,
initial_context=None,
background_context=None,
)
@ -319,7 +319,7 @@ def test_ask_with_deep():
cite=False,
deep=True,
filter=None,
initial_context=None,
background_context=None,
)
@ -337,7 +337,7 @@ def test_ask_with_deep_and_cite():
cite=True,
deep=True,
filter=None,
initial_context=None,
background_context=None,
)