diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a736c4f..765765cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ ### Fixed - **Citation formatting**: Replace raw UUIDs (`[doc_id:chunk_id]`) with human-readable identifiers (`[index] title`) in `format_citations()` output, preventing LLMs from hallucinating opaque ID markers in answers +- **domain_preamble propagation**: `domain_preamble` now flows to skill subagents and the main agent preamble, not just internal agents (QA, research). Fixes ambiguous queries failing when domain context was needed. + +### Changed + +- **domain_preamble docs**: Clarified that `domain_preamble` is for domain context (subject matter, terminology), not behavioral instructions (tone, response style). ## [0.36.2] - 2026-03-28 diff --git a/app/backend/main.py b/app/backend/main.py index c48c535b..29a28957 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -16,7 +16,7 @@ from starlette.routing import Route from haiku.rag.client import HaikuRAG from haiku.rag.config import load_yaml_config from haiku.rag.config.models import AppConfig -from haiku.rag.skills.rag import AGENT_PREAMBLE, create_skill +from haiku.rag.skills.rag import create_skill, get_agent_preamble from haiku.rag.utils import get_model from haiku.skills import ( SkillDeps, @@ -74,7 +74,9 @@ toolset = SkillToolset(skills=[skill]) agent = Agent( get_model(Config.qa.model, Config), - instructions=build_system_prompt(toolset.skill_catalog, preamble=AGENT_PREAMBLE), + instructions=build_system_prompt( + toolset.skill_catalog, preamble=get_agent_preamble(Config) + ), toolsets=[toolset], deps_type=SkillDeps, ) diff --git a/docs/configuration/prompts.md b/docs/configuration/prompts.md index e4c428cf..33cebe4e 100644 --- a/docs/configuration/prompts.md +++ b/docs/configuration/prompts.md @@ -6,10 +6,11 @@ Customize the prompts used by haiku.rag's AI agents to better match your domain ```yaml prompts: - # Prepended to all agent prompts + # Domain context prepended to all agent prompts domain_preamble: | - You are answering questions about our internal documentation. - Technical terms like "time travel" refer to database versioning features. + This knowledge base contains technical documentation for the Helios solar panel + system, including installation manuals, maintenance procedures, and safety guidelines. + Questions about "the system" or unqualified specs refer to the Helios panel. # Full replacement for QA agent prompt (optional) qa: null @@ -23,20 +24,22 @@ prompts: ## Domain Preamble -The `domain_preamble` field is prepended to **all** agent prompts (QA, research planning, search, evaluation, and synthesis). Use this to: +The `domain_preamble` field provides **domain context** that is prepended to all agent prompts — the main agent, skill subagents, and internal agents (QA, research planning, search, evaluation, and synthesis). Use this to: -- Add domain context that clarifies terminology -- Set the tone or personality of responses -- Specify what the knowledge base contains +- Describe what the knowledge base contains +- Clarify domain-specific terminology +- Provide context that helps agents interpret ambiguous queries + +**Important:** `domain_preamble` is for domain context, not behavioral instructions. Descriptions of subject matter, terminology, and content scope belong here. Behavioral guidance (tone, response style, formatting rules) belongs in the agent's system prompt or custom `prompts.qa`. **Example:** ```yaml prompts: domain_preamble: | - You are a technical support assistant for Acme Corp products. - The knowledge base contains product documentation, FAQs, and troubleshooting guides. - Always be helpful and professional. + This knowledge base contains product documentation, API references, + and troubleshooting guides for Acme Corp's cloud platform. + "Deployment" refers to Acme's managed deployment service, not general CI/CD. ``` ## Custom QA Prompt @@ -126,7 +129,7 @@ from haiku.rag.config.models import PromptsConfig config = AppConfig( prompts=PromptsConfig( - domain_preamble="You are answering questions about our product documentation.", + domain_preamble="This knowledge base contains Acme Corp product documentation and API references.", qa=None, # Use default QA prompt synthesis=None, # Use default synthesis prompt picture_description="Describe this image for search indexing.", diff --git a/docs/tuning.md b/docs/tuning.md index 4dedf3d0..efcd0c7f 100644 --- a/docs/tuning.md +++ b/docs/tuning.md @@ -32,7 +32,7 @@ When configured, a cross-encoder reranker re-scores 10x the requested candidates Model and temperature selection affect answer quality directly — see [Providers](configuration/providers.md#model-settings) for options. -`domain_preamble` prepends domain context to all agent prompts. Use it to clarify terminology, set tone, or describe what the knowledge base contains. For full prompt replacement, set `prompts.qa` directly. See [Prompt Customization](configuration/prompts.md). +`domain_preamble` prepends domain context to all agent prompts — including the main agent, skill subagents, and internal agents (QA, research). Use it to describe what the knowledge base contains and clarify domain-specific terminology. For full prompt replacement, set `prompts.qa` directly. See [Prompt Customization](configuration/prompts.md). For automated prompt optimization, see [Prompt Optimization (GEPA)](#prompt-optimization-gepa) below. diff --git a/haiku_rag_slim/haiku/rag/chat/app.py b/haiku_rag_slim/haiku/rag/chat/app.py index 1905510a..da58774f 100644 --- a/haiku_rag_slim/haiku/rag/chat/app.py +++ b/haiku_rag_slim/haiku/rag/chat/app.py @@ -30,7 +30,7 @@ from textual.worker import Worker from haiku.rag.chat.widgets.chat_history import ChatHistory, CitationWidget from haiku.rag.client import HaikuRAG from haiku.rag.config import get_config -from haiku.rag.skills.rag import AGENT_PREAMBLE, RAGState +from haiku.rag.skills.rag import RAGState, get_agent_preamble from haiku.skills.agent import ( SkillToolset, run_agui_stream, @@ -157,7 +157,8 @@ class ChatApp(App): self._agent = Agent( self._model, instructions=build_system_prompt( - self._toolset.skill_catalog, preamble=AGENT_PREAMBLE + self._toolset.skill_catalog, + preamble=get_agent_preamble(self.config), ), toolsets=[self._toolset], ) diff --git a/haiku_rag_slim/haiku/rag/skills/rag.py b/haiku_rag_slim/haiku/rag/skills/rag.py index db3e82c6..e9e52772 100644 --- a/haiku_rag_slim/haiku/rag/skills/rag.py +++ b/haiku_rag_slim/haiku/rag/skills/rag.py @@ -24,6 +24,13 @@ CRITICAL RULES: _RAG_TOOLS = ["search", "list_documents", "get_document", "ask", "research"] +def get_agent_preamble(config: AppConfig) -> str: + """Build the main agent preamble, prepending domain_preamble if configured.""" + if config.prompts.domain_preamble: + return f"{config.prompts.domain_preamble}\n\n{AGENT_PREAMBLE}" + return AGENT_PREAMBLE + + class RAGState(BaseModel): citations: list[Citation] = Field(default_factory=list) qa_history: list[QAHistoryEntry] = Field(default_factory=list) @@ -88,11 +95,15 @@ def create_skill( tools = create_skill_tools(db_path, config, RAGState, _RAG_TOOLS) extras = create_skill_extras(db_path, config) + skill_instructions = instructions() + if config.prompts.domain_preamble and skill_instructions: + skill_instructions = f"{config.prompts.domain_preamble}\n\n{skill_instructions}" + return Skill( metadata=skill_metadata(), source=SkillSource.ENTRYPOINT, path=_skill_path, - instructions=instructions(), + instructions=skill_instructions, tools=list(tools.values()), extras=extras, state_type=STATE_TYPE, diff --git a/haiku_rag_slim/haiku/rag/skills/rlm.py b/haiku_rag_slim/haiku/rag/skills/rlm.py index 7efa403e..503d9d4a 100644 --- a/haiku_rag_slim/haiku/rag/skills/rlm.py +++ b/haiku_rag_slim/haiku/rag/skills/rlm.py @@ -69,11 +69,15 @@ def create_skill( tools = create_skill_tools(db_path, config, RLMState, ["analyze"]) extras = create_skill_extras(db_path, config) + skill_instructions = instructions() + if config.prompts.domain_preamble and skill_instructions: + skill_instructions = f"{config.prompts.domain_preamble}\n\n{skill_instructions}" + return Skill( metadata=skill_metadata(), source=SkillSource.ENTRYPOINT, path=_skill_path, - instructions=instructions(), + instructions=skill_instructions, tools=list(tools.values()), extras=extras, state_type=STATE_TYPE, diff --git a/tests/skills/test_rag.py b/tests/skills/test_rag.py index a82aff31..546425fc 100644 --- a/tests/skills/test_rag.py +++ b/tests/skills/test_rag.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock from haiku.rag.agents.research.models import Citation, ResearchReport from haiku.rag.client import HaikuRAG +from haiku.rag.config.models import AppConfig from haiku.rag.skills.rag import ( STATE_NAMESPACE, STATE_TYPE, @@ -52,6 +53,55 @@ class TestRAGModuleAPI: assert skill.instructions == instructions() +class TestGetAgentPreamble: + def test_without_domain_preamble(self): + from haiku.rag.skills.rag import AGENT_PREAMBLE, get_agent_preamble + + config = AppConfig() + assert get_agent_preamble(config) == AGENT_PREAMBLE + + def test_with_domain_preamble(self): + from haiku.rag.config.models import PromptsConfig + from haiku.rag.skills.rag import AGENT_PREAMBLE, get_agent_preamble + + config = AppConfig( + prompts=PromptsConfig( + domain_preamble="This knowledge base contains Helios solar panel documentation." + ) + ) + result = get_agent_preamble(config) + assert result.startswith( + "This knowledge base contains Helios solar panel documentation." + ) + assert AGENT_PREAMBLE in result + + +class TestDomainPreambleInSkillInstructions: + def test_create_skill_without_domain_preamble(self, test_app_config, temp_db_path): + from haiku.rag.skills.rag import create_skill, instructions + + skill = create_skill(config=test_app_config, db_path=temp_db_path) + assert skill.instructions == instructions() + + def test_create_skill_with_domain_preamble(self, temp_db_path): + from haiku.rag.config.models import PromptsConfig + from haiku.rag.skills.rag import create_skill, instructions + + config = AppConfig( + prompts=PromptsConfig( + domain_preamble="This knowledge base contains Helios solar panel documentation." + ) + ) + skill = create_skill(config=config, db_path=temp_db_path) + assert skill.instructions is not None + assert skill.instructions.startswith( + "This knowledge base contains Helios solar panel documentation." + ) + base_instructions = instructions() + assert base_instructions is not None + assert base_instructions in skill.instructions + + class TestRAGSkillCreation: def test_create_skill_returns_valid_skill(self, test_app_config, temp_db_path): from haiku.rag.skills.rag import create_skill diff --git a/tests/skills/test_rlm.py b/tests/skills/test_rlm.py index c106222a..0b803014 100644 --- a/tests/skills/test_rlm.py +++ b/tests/skills/test_rlm.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock from haiku.rag.agents.rlm.models import RLMResult from haiku.rag.client import HaikuRAG +from haiku.rag.config.models import AppConfig from haiku.rag.skills.rlm import ( STATE_NAMESPACE, STATE_TYPE, @@ -91,6 +92,32 @@ class TestRLMSkillCreation: assert skill.metadata.name == "rag-rlm" +class TestDomainPreambleInRLMSkillInstructions: + def test_create_skill_without_domain_preamble(self, test_app_config, temp_db_path): + from haiku.rag.skills.rlm import create_skill, instructions + + skill = create_skill(config=test_app_config, db_path=temp_db_path) + assert skill.instructions == instructions() + + def test_create_skill_with_domain_preamble(self, temp_db_path): + from haiku.rag.config.models import PromptsConfig + from haiku.rag.skills.rlm import create_skill, instructions + + config = AppConfig( + prompts=PromptsConfig( + domain_preamble="This knowledge base contains Helios solar panel documentation." + ) + ) + skill = create_skill(config=config, db_path=temp_db_path) + assert skill.instructions is not None + assert skill.instructions.startswith( + "This knowledge base contains Helios solar panel documentation." + ) + base_instructions = instructions() + assert base_instructions is not None + assert base_instructions in skill.instructions + + class TestAnalyzeTool: async def test_analyze_returns_result(self, rag_db, monkeypatch): from haiku.rag.skills.rlm import create_skill