From e73486b7a84be2dab76d936fafac80d0eef8906c Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Thu, 15 Jan 2026 17:09:44 +0200 Subject: [PATCH 01/11] Set retries to 3 for agents that did not have it. --- haiku_rag_slim/haiku/rag/agents/chat/agent.py | 1 + haiku_rag_slim/haiku/rag/agents/chat/search.py | 1 + 2 files changed, 2 insertions(+) diff --git a/haiku_rag_slim/haiku/rag/agents/chat/agent.py b/haiku_rag_slim/haiku/rag/agents/chat/agent.py index 636eb08e..4417c5b6 100644 --- a/haiku_rag_slim/haiku/rag/agents/chat/agent.py +++ b/haiku_rag_slim/haiku/rag/agents/chat/agent.py @@ -29,6 +29,7 @@ def create_chat_agent(config: AppConfig) -> Agent[ChatDeps, str]: deps_type=ChatDeps, output_type=str, instructions=CHAT_SYSTEM_PROMPT, + retries=3, ) @agent.tool diff --git a/haiku_rag_slim/haiku/rag/agents/chat/search.py b/haiku_rag_slim/haiku/rag/agents/chat/search.py index 5c71f31b..a6168539 100644 --- a/haiku_rag_slim/haiku/rag/agents/chat/search.py +++ b/haiku_rag_slim/haiku/rag/agents/chat/search.py @@ -21,6 +21,7 @@ class SearchAgent: deps_type=SearchDeps, output_type=str, instructions=SEARCH_SYSTEM_PROMPT, + retries=3, ) @self._agent.tool From dff489f75fc468caa930c74f457f166a3d2d6cb8 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 16 Jan 2026 10:54:02 +0200 Subject: [PATCH 02/11] Add initial_context support to chat agent --- haiku_rag_slim/haiku/rag/agents/chat/agent.py | 22 +++++++ haiku_rag_slim/haiku/rag/agents/chat/state.py | 1 + .../haiku/rag/agents/research/dependencies.py | 4 ++ .../haiku/rag/agents/research/graph.py | 7 ++- tests/agents/chat/test_chat_agent.py | 11 ++++ tests/agents/chat/test_state.py | 19 ++++++ tests/agents/research/test_research_graph.py | 59 +++++++++++++++++++ 7 files changed, 122 insertions(+), 1 deletion(-) diff --git a/haiku_rag_slim/haiku/rag/agents/chat/agent.py b/haiku_rag_slim/haiku/rag/agents/chat/agent.py index 4417c5b6..f676c691 100644 --- a/haiku_rag_slim/haiku/rag/agents/chat/agent.py +++ b/haiku_rag_slim/haiku/rag/agents/chat/agent.py @@ -32,6 +32,13 @@ def create_chat_agent(config: AppConfig) -> Agent[ChatDeps, str]: retries=3, ) + @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}" + return "" + @agent.tool async def search( ctx: RunContext[ChatDeps], @@ -86,6 +93,11 @@ 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 + if ctx.deps.session_state + else None + ), ) # Return detailed results for the agent to present @@ -182,9 +194,14 @@ 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 + ) + context = ResearchContext( original_question=question, qa_responses=existing_qa, + initial_context=initial_context, ) state = ResearchState( context=context, @@ -238,6 +255,11 @@ 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 + if ctx.deps.session_state + else None + ), ) # Format answer with citation references and confidence diff --git a/haiku_rag_slim/haiku/rag/agents/chat/state.py b/haiku_rag_slim/haiku/rag/agents/chat/state.py index 7708df89..f60169df 100644 --- a/haiku_rag_slim/haiku/rag/agents/chat/state.py +++ b/haiku_rag_slim/haiku/rag/agents/chat/state.py @@ -61,6 +61,7 @@ class ChatSessionState(BaseModel): session_id: str = "" citations: list[CitationInfo] = [] qa_history: list[QAResponse] = [] + initial_context: str | None = None def format_conversation_context(qa_history: list[QAResponse]) -> str: diff --git a/haiku_rag_slim/haiku/rag/agents/research/dependencies.py b/haiku_rag_slim/haiku/rag/agents/research/dependencies.py index 12f1f154..1f5559ae 100644 --- a/haiku_rag_slim/haiku/rag/agents/research/dependencies.py +++ b/haiku_rag_slim/haiku/rag/agents/research/dependencies.py @@ -19,6 +19,10 @@ class ResearchContext(BaseModel): qa_responses: list[Any] = Field( default_factory=list, description="Structured QA pairs used during research" ) + initial_context: str | None = Field( + default=None, + description="Optional background context provided at session start", + ) def add_qa_response(self, qa: "SearchAnswer") -> None: """Add a structured QA response.""" diff --git a/haiku_rag_slim/haiku/rag/agents/research/graph.py b/haiku_rag_slim/haiku/rag/agents/research/graph.py index 6e57c582..905da165 100644 --- a/haiku_rag_slim/haiku/rag/agents/research/graph.py +++ b/haiku_rag_slim/haiku/rag/agents/research/graph.py @@ -31,7 +31,7 @@ from haiku.rag.utils import build_prompt, get_model def format_context_for_prompt(context: ResearchContext) -> str: """Format the research context as XML for inclusion in prompts.""" - context_data = { + context_data: dict[str, object] = { "original_question": context.original_question, "unanswered_questions": context.sub_questions, "qa_responses": [ @@ -52,6 +52,8 @@ def format_context_for_prompt(context: ResearchContext) -> str: for qa in context.qa_responses ], } + if context.initial_context: + context_data["initial_context"] = context.initial_context return format_as_xml(context_data, root_tag="research_context") @@ -61,6 +63,9 @@ def format_conversational_context_for_prompt(context: ResearchContext) -> str: "question": context.original_question, } + if context.initial_context: + context_data["initial_context"] = context.initial_context + # Only include conversation_history if there are qa_responses if context.qa_responses: context_data["conversation_history"] = [ diff --git a/tests/agents/chat/test_chat_agent.py b/tests/agents/chat/test_chat_agent.py index 7bb2ac43..77e8d0a8 100644 --- a/tests/agents/chat/test_chat_agent.py +++ b/tests/agents/chat/test_chat_agent.py @@ -76,6 +76,17 @@ def test_chat_session_state(): assert state.qa_history == [] +def test_chat_agent_has_dynamic_system_prompt(): + """Test that chat agent registers a dynamic system prompt for initial_context.""" + agent = create_chat_agent(Config) + # The agent should have at least one system prompt function registered + # (the add_initial_context function) + assert len(agent._system_prompt_functions) >= 1 + # Verify it's the add_initial_context function + func_names = [r.function.__name__ for r in agent._system_prompt_functions] + assert "add_initial_context" in func_names + + def test_citation_info(): """Test CitationInfo model.""" citation = CitationInfo( diff --git a/tests/agents/chat/test_state.py b/tests/agents/chat/test_state.py index df37728d..60f59855 100644 --- a/tests/agents/chat/test_state.py +++ b/tests/agents/chat/test_state.py @@ -248,3 +248,22 @@ def test_build_document_filter_escapes_quotes(): def test_max_qa_history_constant(): """Test MAX_QA_HISTORY constant value.""" assert MAX_QA_HISTORY == 50 + + +def test_chat_session_state_initial_context(): + """Test ChatSessionState accepts initial_context.""" + from haiku.rag.agents.chat.state import ChatSessionState + + state = ChatSessionState( + session_id="test-session", + initial_context="This is background knowledge about the topic.", + ) + assert state.initial_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.""" + from haiku.rag.agents.chat.state import ChatSessionState + + state = ChatSessionState(session_id="test-session") + assert state.initial_context is None diff --git a/tests/agents/research/test_research_graph.py b/tests/agents/research/test_research_graph.py index f2f292ef..c077f73c 100644 --- a/tests/agents/research/test_research_graph.py +++ b/tests/agents/research/test_research_graph.py @@ -44,3 +44,62 @@ async def test_graph_end_to_end(allow_model_requests, temp_db_path, qa_corpus): assert result.executive_summary client.close() + + +def test_research_context_initial_context(): + """Test ResearchContext accepts initial_context.""" + context = ResearchContext( + original_question="What is X?", + initial_context="Background: X is a concept in domain Y.", + ) + assert context.initial_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.""" + context = ResearchContext(original_question="What is X?") + assert context.initial_context is None + + +def test_format_context_for_prompt_includes_initial_context(): + """Test format_context_for_prompt includes initial_context in output.""" + from haiku.rag.agents.research.graph import format_context_for_prompt + + context = ResearchContext( + original_question="What is X?", + initial_context="Background: X is a concept in domain Y.", + ) + result = format_context_for_prompt(context) + assert "Background: X is a concept in domain Y." in result + assert "initial_context" in result + + +def test_format_context_for_prompt_excludes_initial_context_when_none(): + """Test format_context_for_prompt excludes initial_context when None.""" + from haiku.rag.agents.research.graph import format_context_for_prompt + + context = ResearchContext(original_question="What is X?") + result = format_context_for_prompt(context) + assert "initial_context" not in result + + +def test_format_conversational_context_for_prompt_includes_initial_context(): + """Test format_conversational_context_for_prompt includes initial_context.""" + from haiku.rag.agents.research.graph import format_conversational_context_for_prompt + + context = ResearchContext( + original_question="What is X?", + initial_context="Background: X is a concept in domain Y.", + ) + result = format_conversational_context_for_prompt(context) + assert "Background: X is a concept in domain Y." in result + assert "initial_context" in result + + +def test_format_conversational_context_for_prompt_excludes_initial_context_when_none(): + """Test format_conversational_context_for_prompt excludes initial_context when None.""" + from haiku.rag.agents.research.graph import format_conversational_context_for_prompt + + context = ResearchContext(original_question="What is X?") + result = format_conversational_context_for_prompt(context) + assert "initial_context" not in result From d82e1f95c189c1735c620dbcfaecc7d10a89c5e9 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 16 Jan 2026 11:39:57 +0200 Subject: [PATCH 03/11] Optimize context prompts --- .../haiku/rag/agents/research/graph.py | 72 +++++++++++-------- .../haiku/rag/agents/research/prompts.py | 25 ++++--- tests/agents/research/test_research_graph.py | 32 ++++----- 3 files changed, 73 insertions(+), 56 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/agents/research/graph.py b/haiku_rag_slim/haiku/rag/agents/research/graph.py index 905da165..93037740 100644 --- a/haiku_rag_slim/haiku/rag/agents/research/graph.py +++ b/haiku_rag_slim/haiku/rag/agents/research/graph.py @@ -30,49 +30,51 @@ from haiku.rag.utils import build_prompt, get_model def format_context_for_prompt(context: ResearchContext) -> str: - """Format the research context as XML for inclusion in prompts.""" - context_data: dict[str, object] = { - "original_question": context.original_question, - "unanswered_questions": context.sub_questions, - "qa_responses": [ + """Format the research context as XML for planning prompts.""" + context_data: dict[str, object] = {} + + if context.initial_context: + context_data["background"] = context.initial_context + + context_data["question"] = context.original_question + + if context.sub_questions: + context_data["pending_questions"] = context.sub_questions + + if context.qa_responses: + context_data["prior_answers"] = [ { "question": qa.query, "answer": qa.answer, "confidence": qa.confidence, - "sources": [ - { - "document_uri": c.document_uri, - "document_title": c.document_title, - "page_numbers": c.page_numbers, - "headings": c.headings, - } - for c in qa.citations - ], + "source": qa.citations[0].document_title or qa.citations[0].document_uri + if qa.citations + else None, } for qa in context.qa_responses - ], - } - if context.initial_context: - context_data["initial_context"] = context.initial_context - return format_as_xml(context_data, root_tag="research_context") + ] + + return format_as_xml(context_data, root_tag="context") def format_conversational_context_for_prompt(context: ResearchContext) -> str: - """Format context for conversational mode - excludes unanswered_questions.""" - context_data: dict[str, object] = { - "question": context.original_question, - } + """Format context for synthesis prompts.""" + context_data: dict[str, object] = {} if context.initial_context: - context_data["initial_context"] = context.initial_context + context_data["background"] = context.initial_context + + context_data["question"] = context.original_question - # Only include conversation_history if there are qa_responses if context.qa_responses: - context_data["conversation_history"] = [ + context_data["prior_answers"] = [ { "question": qa.query, "answer": qa.answer, - "sources": [c.document_title or c.document_uri for c in qa.citations], + "confidence": qa.confidence, + "source": qa.citations[0].document_title or qa.citations[0].document_uri + if qa.citations + else None, } for qa in context.qa_responses ] @@ -95,9 +97,12 @@ async def _plan_step_logic( model_config = config.research.model # Use context-aware prompt if we have existing qa_responses - has_context = bool(state.context.qa_responses) + has_prior_answers = bool(state.context.qa_responses) + has_background = bool(state.context.initial_context) effective_plan_prompt = ( - build_prompt(PLAN_PROMPT_WITH_CONTEXT, config) if has_context else plan_prompt + build_prompt(PLAN_PROMPT_WITH_CONTEXT, config) + if has_prior_answers + else plan_prompt ) plan_agent = Agent( @@ -124,13 +129,20 @@ async def _plan_step_logic( return "\n\n".join(r.content for r in results) # Build prompt with existing context if available - if has_context: + if has_prior_answers: context_xml = format_context_for_prompt(state.context) prompt = ( f"Review existing context and plan additional research if needed.\n\n" f"{context_xml}\n\n" f"Main question: {state.context.original_question}" ) + elif has_background: + context_xml = format_context_for_prompt(state.context) + prompt = ( + f"Plan a focused approach for the main question.\n\n" + f"{context_xml}\n\n" + f"Main question: {state.context.original_question}" + ) else: prompt = ( "Plan a focused approach for the main question.\n\n" diff --git a/haiku_rag_slim/haiku/rag/agents/research/prompts.py b/haiku_rag_slim/haiku/rag/agents/research/prompts.py index b472e18e..6fe97b42 100644 --- a/haiku_rag_slim/haiku/rag/agents/research/prompts.py +++ b/haiku_rag_slim/haiku/rag/agents/research/prompts.py @@ -1,10 +1,11 @@ -PLAN_PROMPT = """You are the research orchestrator for a focused, iterative workflow. +PLAN_PROMPT = """You are the research orchestrator for a focused workflow. + +If a section is provided, use it to understand the domain context. Responsibilities: 1. Understand and decompose the main question 2. Propose a minimal, high-leverage plan 3. Coordinate specialized agents to gather evidence -4. Iterate based on gaps and new findings Plan requirements: - Produce at most 3 sub_questions that together cover the main question. @@ -22,19 +23,22 @@ Use the gather_context tool once on the main question before planning.""" PLAN_PROMPT_WITH_CONTEXT = """You are the research orchestrator for a focused workflow. -You have access to PREVIOUS CONVERSATION CONTEXT in the qa_responses section below. -Review this context first - if it already answers the question, generate minimal -or no sub-questions. Only create sub-questions to fill gaps in the existing context. +You have access to context that may include: +- : Domain context for the conversation +- : Previous Q&A pairs with confidence scores + +Review this first - if prior answers already answer the question completely, +you may return an empty sub_questions list. Only create sub-questions to +fill genuine gaps. Responsibilities: -1. Review existing qa_responses to understand what's already known +1. Review prior_answers to understand what's already known 2. Identify gaps that need additional research 3. Propose minimal sub-questions only for missing information Plan requirements: -- If existing context fully answers the question, return a SINGLE sub-question - to verify or slightly expand the answer. -- Only create new sub-questions for genuine gaps in the existing knowledge. +- If prior answers fully answer the question, return an empty sub_questions list. +- Only create new sub-questions for genuine gaps in existing knowledge. - sub_questions must be a list of plain strings (max 3). - Each sub_question must be standalone and self-contained. - Prioritize the highest-value gaps first. @@ -145,7 +149,8 @@ Output: - confidence: Score from 0.0 to 1.0 indicating answer quality. Guidelines: -- Base your answer solely on the collected evidence in qa_responses. +- Base your answer solely on the evidence provided in the context. +- If a section is provided, use it to frame your answer appropriately. - Be thorough - include all relevant information from the evidence. - Use formatting (bullet points, numbered lists) when it improves clarity. - Do NOT use meta-commentary like "Based on the research..." or "The evidence shows..." diff --git a/tests/agents/research/test_research_graph.py b/tests/agents/research/test_research_graph.py index c077f73c..c95e5203 100644 --- a/tests/agents/research/test_research_graph.py +++ b/tests/agents/research/test_research_graph.py @@ -61,45 +61,45 @@ def test_research_context_initial_context_defaults_to_none(): assert context.initial_context is None -def test_format_context_for_prompt_includes_initial_context(): - """Test format_context_for_prompt includes initial_context in output.""" +def test_format_context_for_prompt_includes_background(): + """Test format_context_for_prompt includes background in output.""" from haiku.rag.agents.research.graph import format_context_for_prompt context = ResearchContext( original_question="What is X?", - initial_context="Background: X is a concept in domain Y.", + initial_context="X is a concept in domain Y.", ) result = format_context_for_prompt(context) - assert "Background: X is a concept in domain Y." in result - assert "initial_context" in result + assert "X is a concept in domain Y." in result + assert "" in result -def test_format_context_for_prompt_excludes_initial_context_when_none(): - """Test format_context_for_prompt excludes initial_context when None.""" +def test_format_context_for_prompt_excludes_background_when_none(): + """Test format_context_for_prompt excludes background when None.""" from haiku.rag.agents.research.graph import format_context_for_prompt context = ResearchContext(original_question="What is X?") result = format_context_for_prompt(context) - assert "initial_context" not in result + assert "" not in result -def test_format_conversational_context_for_prompt_includes_initial_context(): - """Test format_conversational_context_for_prompt includes initial_context.""" +def test_format_conversational_context_for_prompt_includes_background(): + """Test format_conversational_context_for_prompt includes background.""" from haiku.rag.agents.research.graph import format_conversational_context_for_prompt context = ResearchContext( original_question="What is X?", - initial_context="Background: X is a concept in domain Y.", + initial_context="X is a concept in domain Y.", ) result = format_conversational_context_for_prompt(context) - assert "Background: X is a concept in domain Y." in result - assert "initial_context" in result + assert "X is a concept in domain Y." in result + assert "" in result -def test_format_conversational_context_for_prompt_excludes_initial_context_when_none(): - """Test format_conversational_context_for_prompt excludes initial_context when None.""" +def test_format_conversational_context_for_prompt_excludes_background_when_none(): + """Test format_conversational_context_for_prompt excludes background when None.""" from haiku.rag.agents.research.graph import format_conversational_context_for_prompt context = ResearchContext(original_question="What is X?") result = format_conversational_context_for_prompt(context) - assert "initial_context" not in result + assert "" not in result From 18362f01c22bbde6382acde221e6891d55316adc Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 16 Jan 2026 12:40:35 +0200 Subject: [PATCH 04/11] Pass context for CLI/app --- app/backend/main.py | 5 +- haiku_rag_slim/haiku/rag/app.py | 27 +++++++-- haiku_rag_slim/haiku/rag/chat/__init__.py | 6 +- haiku_rag_slim/haiku/rag/chat/app.py | 19 ++++-- haiku_rag_slim/haiku/rag/cli.py | 70 ++++++++++++++++++++++- tests/test_app.py | 8 ++- 6 files changed, 120 insertions(+), 15 deletions(-) diff --git a/app/backend/main.py b/app/backend/main.py index 42a4fa5d..6479f0ef 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -80,8 +80,9 @@ async def stream_chat(request: Request) -> Response: accept = request.headers.get("accept", SSE_CONTENT_TYPE) run_input = AGUIAdapter.build_run_input(body) - # Restore qa_history from incoming state (look under namespaced key) + # Restore session state from incoming AG-UI state (look under namespaced key) initial_qa_history: list[QAResponse] = [] + initial_context: str | None = None state = getattr(run_input, "state", None) if state: chat_state = state.get(AGUI_STATE_KEY, state) @@ -89,6 +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") # Build deps with session state thread_id = getattr(run_input, "thread_id", None) @@ -98,6 +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, ), state_key=AGUI_STATE_KEY, ) diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py index 6305bc70..1167b61f 100644 --- a/haiku_rag_slim/haiku/rag/app.py +++ b/haiku_rag_slim/haiku/rag/app.py @@ -376,6 +376,7 @@ class HaikuRAGApp: cite: bool = False, deep: bool = False, filter: str | None = None, + initial_context: str | None = None, ): """Ask a question using the RAG system. @@ -384,6 +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 """ async with HaikuRAG( db_path=self.db_path, @@ -394,7 +396,9 @@ class HaikuRAGApp: citations = [] if deep: graph = build_research_graph(config=self.config) - context = ResearchContext(original_question=question) + context = ResearchContext( + original_question=question, initial_context=initial_context + ) state = ResearchState.from_config( context=context, config=self.config, @@ -423,7 +427,14 @@ class HaikuRAGApp: else: self.console.print("[yellow]No answer generated.[/yellow]") else: - answer, citations = await self.client.ask(question, filter=filter) + system_prompt = ( + f"BACKGROUND CONTEXT:\n{initial_context}" + if initial_context + else None + ) + answer, citations = await self.client.ask( + question, system_prompt=system_prompt, filter=filter + ) self.console.print(f"[bold blue]Question:[/bold blue] {question}") self.console.print() @@ -433,12 +444,18 @@ class HaikuRAGApp: for renderable in format_citations_rich(citations): self.console.print(renderable) - async def research(self, question: str, filter: str | None = None): + async def research( + self, + question: str, + filter: str | None = None, + initial_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 """ async with HaikuRAG( db_path=self.db_path, @@ -451,7 +468,9 @@ class HaikuRAGApp: self.console.print() graph = build_research_graph(config=self.config) - context = ResearchContext(original_question=question) + context = ResearchContext( + original_question=question, initial_context=initial_context + ) state = ResearchState.from_config(context=context, config=self.config) state.search_filter = filter deps = ResearchDeps(client=client) diff --git a/haiku_rag_slim/haiku/rag/chat/__init__.py b/haiku_rag_slim/haiku/rag/chat/__init__.py index 6045a8b2..38c5a297 100644 --- a/haiku_rag_slim/haiku/rag/chat/__init__.py +++ b/haiku_rag_slim/haiku/rag/chat/__init__.py @@ -6,6 +6,7 @@ def run_chat( db_path: Path | None = None, read_only: bool = False, before: datetime | None = None, + initial_context: str | None = None, ) -> None: """Run the chat TUI. @@ -13,6 +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. """ try: from haiku.rag.chat.app import ChatApp @@ -27,5 +29,7 @@ def run_chat( if db_path is None: db_path = config.storage.data_dir / "haiku.rag.lancedb" - app = ChatApp(db_path, read_only=read_only, before=before) + app = ChatApp( + db_path, read_only=read_only, before=before, initial_context=initial_context + ) app.run() diff --git a/haiku_rag_slim/haiku/rag/chat/app.py b/haiku_rag_slim/haiku/rag/chat/app.py index 94b4f68a..c2dbf1cc 100644 --- a/haiku_rag_slim/haiku/rag/chat/app.py +++ b/haiku_rag_slim/haiku/rag/chat/app.py @@ -85,12 +85,17 @@ class ChatApp(App): # type: ignore[misc] ] def __init__( - self, db_path: Path, read_only: bool = False, before: datetime | None = None + self, + db_path: Path, + read_only: bool = False, + before: datetime | None = None, + initial_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.client: HaikuRAG | None = None self.config = get_config() self.agent: Agent[ChatDeps, str] | None = None @@ -121,7 +126,10 @@ class ChatApp(App): # type: ignore[misc] # Create agent and session state self.agent = create_chat_agent(self.config) - self.session_state = ChatSessionState(session_id=str(uuid.uuid4())) + self.session_state = ChatSessionState( + session_id=str(uuid.uuid4()), + initial_context=self.initial_context, + ) # Focus the input field self.query_one(Input).focus() @@ -266,8 +274,11 @@ 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 - self.session_state = ChatSessionState(session_id=str(uuid.uuid4())) + # Reset session state for fresh conversation (preserve initial_context) + self.session_state = ChatSessionState( + session_id=str(uuid.uuid4()), + initial_context=self.initial_context, + ) def action_focus_input(self) -> None: """Focus the input field, or cancel if processing.""" diff --git a/haiku_rag_slim/haiku/rag/cli.py b/haiku_rag_slim/haiku/rag/cli.py index 11649c17..2cc84dfc 100644 --- a/haiku_rag_slim/haiku/rag/cli.py +++ b/haiku_rag_slim/haiku/rag/cli.py @@ -338,9 +338,34 @@ def ask( "-f", help="SQL WHERE clause to filter documents (e.g., \"uri LIKE '%arxiv%'\")", ), + context: str | None = typer.Option( + None, + "--context", + help="Background context for the question", + ), + context_file: Path | None = typer.Option( + None, + "--context-file", + help="Path to a file containing background context", + ), ): + # Resolve initial context from flag or file + initial_context: str | None = None + if context_file: + initial_context = context_file.read_text() + elif context: + initial_context = context + app = create_app(db) - asyncio.run(app.ask(question=question, cite=cite, deep=deep, filter=filter)) + asyncio.run( + app.ask( + question=question, + cite=cite, + deep=deep, + filter=filter, + initial_context=initial_context, + ) + ) @cli.command("research", help="Run multi-agent research and output a concise report") @@ -357,9 +382,28 @@ def research( "-f", help="SQL WHERE clause to filter documents (e.g., \"uri LIKE '%arxiv%'\")", ), + context: str | None = typer.Option( + None, + "--context", + help="Background context for the research", + ), + context_file: Path | None = typer.Option( + None, + "--context-file", + help="Path to a file containing background context", + ), ): + # Resolve initial context from flag or file + initial_context: str | None = None + if context_file: + initial_context = context_file.read_text() + elif context: + initial_context = context + app = create_app(db) - asyncio.run(app.research(question=question, filter=filter)) + asyncio.run( + app.research(question=question, filter=filter, initial_context=initial_context) + ) @cli.command("settings", help="Display current configuration settings") @@ -547,12 +591,32 @@ def chat( "--db", help="Path to the LanceDB database file", ), + context: str | None = typer.Option( + None, + "--context", + help="Initial context/background information for the conversation", + ), + context_file: Path | None = typer.Option( + None, + "--context-file", + help="Path to a file containing initial context", + ), ): """Launch the chat TUI for conversational RAG.""" from haiku.rag.chat import run_chat db_path = db if db else get_config().storage.data_dir / "haiku.rag.lancedb" - run_chat(db_path, read_only=_read_only, before=_before) + + # Resolve initial context from flag or file + initial_context: str | None = None + if context_file: + initial_context = context_file.read_text() + elif context: + initial_context = context + + run_chat( + db_path, read_only=_read_only, before=_before, initial_context=initial_context + ) @cli.command( diff --git a/tests/test_app.py b/tests/test_app.py index 8443ba36..7e794e6a 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -304,7 +304,9 @@ async def test_ask_without_cite(app: HaikuRAGApp, monkeypatch): with patch("haiku.rag.app.HaikuRAG", return_value=mock_client): await app.ask("test question") - mock_client.ask.assert_called_once_with("test question", filter=None) + mock_client.ask.assert_called_once_with( + "test question", system_prompt=None, filter=None + ) @pytest.mark.asyncio @@ -333,7 +335,9 @@ async def test_ask_with_cite(app: HaikuRAGApp, monkeypatch): with patch("haiku.rag.app.HaikuRAG", return_value=mock_client): await app.ask("test question", cite=True) - mock_client.ask.assert_called_once_with("test question", filter=None) + mock_client.ask.assert_called_once_with( + "test question", system_prompt=None, filter=None + ) # Verify print was called (once for answer, once for citations) assert mock_print.call_count >= 1 From d2fabb9f13cb3f90765a7287d3233fffc85d9391 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 16 Jan 2026 12:47:16 +0200 Subject: [PATCH 05/11] Update docs --- CHANGELOG.md | 10 ++++++++++ docs/agents.md | 26 ++++++++++++++++++++++++++ docs/cli.md | 29 +++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0801be0d..30daf3f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Changelog ## [Unreleased] +### Added + +- **Background Context Support**: Pass initial 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 + - Context is included in agent system prompts and research graph planning + ## [0.26.4] - 2026-01-15 ### Added diff --git a/docs/agents.md b/docs/agents.md index 70c436f1..02121300 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -103,6 +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 - `embedding_cache` — Cached embeddings for semantic ranking Q/A history is used to: @@ -111,6 +112,19 @@ Q/A history is used to: 2. Avoid repeating previous answers 3. Enable semantic ranking of relevant past answers +### Background Context + +You can provide background context that persists throughout the conversation: + +```python +session = ChatSessionState( + initial_context="Focus on Python programming concepts and best practices." +) +deps = ChatDeps(client=client, config=config, session_state=session) +``` + +The context is included in the agent's system prompt and passed to the research graph when answering questions. + ### AG-UI Integration When using the chat agent with AG-UI streaming, state is emitted under a namespaced key to avoid conflicts with other agents: @@ -216,6 +230,18 @@ async with HaikuRAG(path_to_db) as client: print(report.executive_summary) ``` +**With background context:** + +```python +context = ResearchContext( + original_question="What are the safety protocols?", + initial_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. + **With custom config:** ```python diff --git a/docs/cli.md b/docs/cli.md index f4b94367..4b733ef5 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -153,6 +153,12 @@ Filter to specific documents: haiku-rag ask "What are the main findings?" --filter "uri LIKE '%paper%'" ``` +Provide background context for the question: +```bash +haiku-rag ask "What are the protocols?" --context "Focus on security best practices" +haiku-rag ask "Summarize the findings" --context-file background.txt +``` + The QA agent searches your documents for relevant information and provides a comprehensive answer. When available, citations use the document title; otherwise they fall back to the URI. Flags: @@ -160,6 +166,8 @@ Flags: - `--cite`: Include citations showing which documents were used - `--deep`: Decompose the question into sub-questions answered in parallel before synthesizing a final answer - `--filter` / `-f`: Restrict searches to documents matching the filter (see [Filtering Search Results](python.md#filtering-search-results)) +- `--context`: Background context for the question (passed to the agent as system context) +- `--context-file`: Path to a file containing background context ## Chat @@ -170,6 +178,12 @@ haiku-rag chat haiku-rag chat --db /path/to/database.lancedb ``` +Provide background context for the conversation: +```bash +haiku-rag chat --context "Focus on Python programming concepts" +haiku-rag chat --context-file domain-context.txt +``` + !!! note Requires the `tui` extra: `pip install haiku.rag-slim[tui]` (included in full `haiku.rag` package) @@ -179,6 +193,12 @@ The chat interface provides: - Expandable citations with source metadata - Session memory for context-aware follow-up questions - Visual grounding to inspect chunk source locations +- Background context that persists across the entire conversation + +Flags: + +- `--context`: Background context for the conversation +- `--context-file`: Path to a file containing background context See [Applications](apps.md#chat-tui) for keyboard shortcuts and features. @@ -217,9 +237,18 @@ Filter to specific documents: haiku-rag research "What are the key findings?" --filter "uri LIKE '%paper%'" ``` +Provide background context for the research: + +```bash +haiku-rag research "What are the safety protocols?" --context "Industrial manufacturing context" +haiku-rag research "Analyze the methodology" --context-file research-background.txt +``` + Flags: - `--filter` / `-f`: SQL WHERE clause to filter documents (see [Filtering Search Results](python.md#filtering-search-results)) +- `--context`: Background context for the research +- `--context-file`: Path to a file containing background context Research parameters like `max_iterations`, `confidence_threshold`, and `max_concurrency` are configured in your [configuration file](configuration/index.md) under the `research` section. From 205445014229c9ed8fbb68bf97587d4046dd0c5a Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 16 Jan 2026 13:55:26 +0200 Subject: [PATCH 06/11] ChatDeps implements StateHandler protocol for proper AG-UI state management --- haiku_rag_slim/haiku/rag/agents/chat/state.py | 45 +++- tests/agents/chat/test_state.py | 203 ++++++++++++++++++ tests/test_cli.py | 4 + 3 files changed, 250 insertions(+), 2 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/agents/chat/state.py b/haiku_rag_slim/haiku/rag/agents/chat/state.py index f60169df..61c23499 100644 --- a/haiku_rag_slim/haiku/rag/agents/chat/state.py +++ b/haiku_rag_slim/haiku/rag/agents/chat/state.py @@ -1,6 +1,6 @@ import hashlib from dataclasses import dataclass, field -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import numpy as np from numpy.typing import NDArray @@ -156,7 +156,10 @@ async def rank_qa_history_by_similarity( @dataclass class ChatDeps: - """Dependencies for chat agent.""" + """Dependencies for chat agent. + + Implements StateHandler protocol for AG-UI state management. + """ client: HaikuRAG config: AppConfig @@ -164,6 +167,44 @@ class ChatDeps: session_state: ChatSessionState | None = None state_key: str | None = None + @property + def state(self) -> dict[str, Any] | None: + """Get current state for AG-UI protocol.""" + if self.session_state is None: + return None + snapshot = self.session_state.model_dump() + if self.state_key: + return {self.state_key: snapshot} + return snapshot + + @state.setter + def state(self, value: dict[str, Any] | None) -> None: + """Set state from AG-UI protocol.""" + if value is None: + return + # Extract from namespaced key if present + state_data: dict[str, Any] = value + if self.state_key and self.state_key in value: + nested = value[self.state_key] + if isinstance(nested, dict): + state_data = nested + # Update session_state from incoming state + if self.session_state is not None: + if "qa_history" in state_data: + self.session_state.qa_history = [ + QAResponse(**qa) if isinstance(qa, dict) else qa + for qa in state_data.get("qa_history", []) + ] + if "citations" in state_data: + self.session_state.citations = [ + 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 "session_id" in state_data: + self.session_state.session_id = state_data.get("session_id", "") + @dataclass class SearchDeps: diff --git a/tests/agents/chat/test_state.py b/tests/agents/chat/test_state.py index 60f59855..5e933648 100644 --- a/tests/agents/chat/test_state.py +++ b/tests/agents/chat/test_state.py @@ -267,3 +267,206 @@ def test_chat_session_state_initial_context_defaults_to_none(): state = ChatSessionState(session_id="test-session") assert state.initial_context is None + + +def test_chat_deps_state_getter_returns_namespaced_state(): + """Test ChatDeps.state getter returns state under namespaced key.""" + from unittest.mock import MagicMock + + from haiku.rag.agents.chat.state import AGUI_STATE_KEY, ChatDeps, ChatSessionState + + mock_client = MagicMock() + mock_config = MagicMock() + + session_state = ChatSessionState( + session_id="test-123", + qa_history=[ + QAResponse(question="Q1", answer="A1", confidence=0.9), + ], + initial_context="Background info", + ) + + deps = ChatDeps( + client=mock_client, + config=mock_config, + session_state=session_state, + state_key=AGUI_STATE_KEY, + ) + + state = deps.state + assert state is not None + assert AGUI_STATE_KEY in 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" + + +def test_chat_deps_state_getter_without_namespace(): + """Test ChatDeps.state getter returns flat state when no state_key.""" + from unittest.mock import MagicMock + + from haiku.rag.agents.chat.state import ChatDeps, ChatSessionState + + mock_client = MagicMock() + mock_config = MagicMock() + + session_state = ChatSessionState(session_id="test-123") + deps = ChatDeps( + client=mock_client, + config=mock_config, + session_state=session_state, + state_key=None, + ) + + state = deps.state + assert state is not None + assert "session_id" in state + assert state["session_id"] == "test-123" + + +def test_chat_deps_state_getter_returns_none_without_session(): + """Test ChatDeps.state getter returns None when no session_state.""" + from unittest.mock import MagicMock + + from haiku.rag.agents.chat.state import ChatDeps + + mock_client = MagicMock() + mock_config = MagicMock() + + deps = ChatDeps( + client=mock_client, + config=mock_config, + session_state=None, + ) + + assert deps.state is None + + +def test_chat_deps_state_setter_updates_from_namespaced_state(): + """Test ChatDeps.state setter updates session_state from namespaced incoming state.""" + from unittest.mock import MagicMock + + from haiku.rag.agents.chat.state import AGUI_STATE_KEY, ChatDeps, ChatSessionState + + mock_client = MagicMock() + mock_config = MagicMock() + + session_state = ChatSessionState(session_id="initial") + deps = ChatDeps( + client=mock_client, + config=mock_config, + session_state=session_state, + state_key=AGUI_STATE_KEY, + ) + + # Simulate incoming AG-UI state with namespaced key + incoming_state = { + AGUI_STATE_KEY: { + "session_id": "updated-123", + "qa_history": [ + {"question": "Q1", "answer": "A1", "confidence": 0.9, "citations": []} + ], + "citations": [], + "initial_context": "New context", + } + } + + deps.state = incoming_state + + assert deps.session_state is not None + 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" + + +def test_chat_deps_state_setter_handles_none(): + """Test ChatDeps.state setter handles None gracefully.""" + from unittest.mock import MagicMock + + from haiku.rag.agents.chat.state import ChatDeps, ChatSessionState + + mock_client = MagicMock() + mock_config = MagicMock() + + session_state = ChatSessionState(session_id="original") + deps = ChatDeps( + client=mock_client, + config=mock_config, + session_state=session_state, + ) + + # Setting None should not raise and should not change state + deps.state = None + + assert deps.session_state is not None + assert deps.session_state.session_id == "original" + + +def test_chat_deps_state_setter_without_session_state(): + """Test ChatDeps.state setter does nothing when session_state is None.""" + from unittest.mock import MagicMock + + from haiku.rag.agents.chat.state import ChatDeps + + mock_client = MagicMock() + mock_config = MagicMock() + + deps = ChatDeps( + client=mock_client, + config=mock_config, + session_state=None, + ) + + # Should not raise even with valid incoming state + deps.state = {"session_id": "test", "qa_history": [], "citations": []} + + assert deps.session_state is None + + +def test_chat_deps_state_setter_with_citation_dicts(): + """Test ChatDeps.state setter converts citation dicts to CitationInfo.""" + from unittest.mock import MagicMock + + from haiku.rag.agents.chat.state import AGUI_STATE_KEY, ChatDeps, ChatSessionState + + mock_client = MagicMock() + mock_config = MagicMock() + + session_state = ChatSessionState(session_id="test") + deps = ChatDeps( + client=mock_client, + config=mock_config, + session_state=session_state, + state_key=AGUI_STATE_KEY, + ) + + incoming_state = { + AGUI_STATE_KEY: { + "session_id": "test", + "qa_history": [], + "citations": [ + { + "index": 1, + "document_id": "doc-1", + "chunk_id": "chunk-1", + "document_uri": "test.md", + "document_title": "Test Doc", + "page_numbers": [1, 2], + "headings": ["Intro"], + "content": "Test content", + } + ], + "initial_context": None, + } + } + + deps.state = incoming_state + + assert deps.session_state is not None + assert len(deps.session_state.citations) == 1 + citation = deps.session_state.citations[0] + assert citation.document_id == "doc-1" + assert citation.chunk_id == "chunk-1" + assert citation.page_numbers == [1, 2] diff --git a/tests/test_cli.py b/tests/test_cli.py index 933d35e5..c92e6882 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -283,6 +283,7 @@ def test_ask(): cite=False, deep=False, filter=None, + initial_context=None, ) @@ -300,6 +301,7 @@ def test_ask_with_cite(): cite=True, deep=False, filter=None, + initial_context=None, ) @@ -317,6 +319,7 @@ def test_ask_with_deep(): cite=False, deep=True, filter=None, + initial_context=None, ) @@ -334,6 +337,7 @@ def test_ask_with_deep_and_cite(): cite=True, deep=True, filter=None, + initial_context=None, ) From 609cbb78deb15521a74855ef5288aa3c4d6532c9 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 16 Jan 2026 14:08:33 +0200 Subject: [PATCH 07/11] Background context component for frontend --- app/frontend/components/Chat.tsx | 115 ++++++++++++- app/frontend/components/SettingsPanel.tsx | 188 ++++++++++++++++++++++ 2 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 app/frontend/components/SettingsPanel.tsx diff --git a/app/frontend/components/Chat.tsx b/app/frontend/components/Chat.tsx index dafca4a9..bf663ed2 100644 --- a/app/frontend/components/Chat.tsx +++ b/app/frontend/components/Chat.tsx @@ -1,5 +1,6 @@ "use client"; +import { useCallback, useEffect, useState } from "react"; import { CopilotKit, useCoAgent, @@ -10,6 +11,7 @@ import { CopilotChat } from "@copilotkit/react-ui"; import "@copilotkit/react-ui/styles.css"; import CitationBlock from "./CitationBlock"; import DbInfo from "./DbInfo"; +import SettingsPanel, { STORAGE_KEY } from "./SettingsPanel"; // Must match AGUI_STATE_KEY from haiku.rag.agents.chat const AGUI_STATE_KEY = "haiku.rag.chat"; @@ -36,6 +38,7 @@ interface ChatSessionState { session_id: string; citations: Citation[]; qa_history: QAResponse[]; + initial_context: string | null; } // AG-UI state is namespaced under AGUI_STATE_KEY @@ -131,6 +134,24 @@ function FileIcon() { ); } +function SettingsIcon() { + return ( + + + + + ); +} + function ToolCallIndicator({ toolName, status, @@ -336,7 +357,27 @@ function ToolCallIndicator({ ); } -function ChatContent() { +function ChatContentInner({ + initialContext, + setInitialContext, +}: { + initialContext: string; + setInitialContext: (value: string) => void; +}) { + const [settingsOpen, setSettingsOpen] = useState(false); + + const handleSaveContext = useCallback( + (value: string) => { + setInitialContext(value); + if (value) { + localStorage.setItem(STORAGE_KEY, value); + } else { + localStorage.removeItem(STORAGE_KEY); + } + }, + [setInitialContext], + ); + useCoAgent({ name: "chat_agent", initialState: { @@ -344,6 +385,7 @@ function ChatContent() { session_id: "", citations: [], qa_history: [], + initial_context: initialContext || null, }, }, }); @@ -425,6 +467,40 @@ function ChatContent() { display: flex; flex-direction: column; } + .chat-header { + display: flex; + justify-content: flex-end; + padding: 0.5rem 0.75rem; + border-bottom: 1px solid #e2e8f0; + background: #f8fafc; + } + .settings-btn { + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.625rem; + background: white; + border: 1px solid #e2e8f0; + border-radius: 6px; + cursor: pointer; + color: #64748b; + font-size: 0.8125rem; + transition: all 0.15s; + } + .settings-btn:hover { + background: #f1f5f9; + border-color: #cbd5e1; + color: #475569; + } + .settings-btn.has-context { + background: #eff6ff; + border-color: #bfdbfe; + color: #2563eb; + } + .settings-btn.has-context:hover { + background: #dbeafe; + border-color: #93c5fd; + } .chat-content { flex: 1; min-height: 0; @@ -438,6 +514,17 @@ function ChatContent() { `}
+
+ +
+ setSettingsOpen(false)} + onSave={handleSaveContext} + currentValue={initialContext} + /> ); } +function ChatContent() { + const [initialContext, setInitialContext] = useState(null); + + useEffect(() => { + const stored = localStorage.getItem(STORAGE_KEY); + setInitialContext(stored || ""); + }, []); + + if (initialContext === null) { + return null; + } + + return ( + + ); +} + export default function Chat() { return ( diff --git a/app/frontend/components/SettingsPanel.tsx b/app/frontend/components/SettingsPanel.tsx new file mode 100644 index 00000000..76638e7b --- /dev/null +++ b/app/frontend/components/SettingsPanel.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +const STORAGE_KEY = "haiku.rag.settings.initial_context"; + +interface SettingsPanelProps { + isOpen: boolean; + onClose: () => void; + onSave: (initialContext: string) => void; + currentValue: string; +} + +export default function SettingsPanel({ + isOpen, + onClose, + onSave, + currentValue, +}: SettingsPanelProps) { + const [value, setValue] = useState(currentValue); + + useEffect(() => { + if (isOpen) { + setValue(currentValue); + } + }, [isOpen, currentValue]); + + const handleSave = useCallback(() => { + onSave(value); + onClose(); + }, [value, onSave, onClose]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }, + [onClose], + ); + + if (!isOpen) { + return null; + } + + return ( + <> + + {/* biome-ignore lint/a11y/useKeyWithClickEvents: handled via onKeyDown on overlay */} +
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: modal content wrapper */} +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > +

+ Background Context +

+

+ Provide background information that will be used throughout your + conversation. This helps the assistant understand domain-specific + context, terminology, or any relevant details about your questions. +

+