diff --git a/README.md b/README.md index c8361a80..c97c15c7 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,9 @@ haiku-rag ask "Who is the author of haiku.rag?" --cite # Deep QA (multi-agent question decomposition) haiku-rag ask "Who is the author of haiku.rag?" --deep --cite +# Deep QA with verbose output +haiku-rag ask "Who is the author of haiku.rag?" --deep --verbose + # Multi‑agent research (iterative plan/search/evaluate) haiku-rag research \ "What are the main drivers and trends of global temperature anomalies since 1990?" \ diff --git a/docs/cli.md b/docs/cli.md index c21386a5..033faa5a 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -97,7 +97,12 @@ Use deep QA for complex questions (multi-agent decomposition): haiku-rag ask "What are the main features and architecture of haiku.rag?" --deep --cite ``` -The QA agent will search your documents for relevant information and provide a comprehensive answer. With `--cite`, responses include citations showing which documents were used. With `--deep`, the question is decomposed into sub-questions that are answered in parallel before synthesizing a final answer. +Show verbose output with deep QA: +```bash +haiku-rag ask "What are the main features and architecture of haiku.rag?" --deep --verbose +``` + +The QA agent will search your documents for relevant information and provide a comprehensive answer. With `--cite`, responses include citations showing which documents were used. With `--deep`, the question is decomposed into sub-questions that are answered in parallel before synthesizing a final answer. With `--verbose` (only with `--deep`), you'll see the planning, searching, evaluation, and synthesis steps as they happen. When available, citations use the document title; otherwise they fall back to the URI. ## Research diff --git a/src/haiku/rag/app.py b/src/haiku/rag/app.py index 247ac882..c5786a96 100644 --- a/src/haiku/rag/app.py +++ b/src/haiku/rag/app.py @@ -194,10 +194,18 @@ class HaikuRAGApp: for chunk, score in results: self._rich_print_search_result(chunk, score) - async def ask(self, question: str, cite: bool = False, deep: bool = False): + async def ask( + self, + question: str, + cite: bool = False, + deep: bool = False, + verbose: bool = False, + ): async with HaikuRAG(db_path=self.db_path) as self.client: try: if deep: + from rich.console import Console + from haiku.rag.qa.deep.dependencies import DeepQAContext from haiku.rag.qa.deep.graph import build_deep_qa_graph from haiku.rag.qa.deep.nodes import DeepPlanNode @@ -208,7 +216,9 @@ class HaikuRAGApp: original_question=question, use_citations=cite ) state = DeepQAState(context=context) - deps = DeepQADeps(client=self.client) + deps = DeepQADeps( + client=self.client, console=Console() if verbose else None + ) start_node = DeepPlanNode( provider=Config.QA_PROVIDER, diff --git a/src/haiku/rag/cli.py b/src/haiku/rag/cli.py index fc4f7d94..62ecf660 100644 --- a/src/haiku/rag/cli.py +++ b/src/haiku/rag/cli.py @@ -304,11 +304,16 @@ def ask( "--deep", help="Use deep multi-agent QA for complex questions", ), + verbose: bool = typer.Option( + False, + "--verbose", + help="Show verbose progress output (only with --deep)", + ), ): from haiku.rag.app import HaikuRAGApp app = HaikuRAGApp(db_path=db) - asyncio.run(app.ask(question=question, cite=cite, deep=deep)) + asyncio.run(app.ask(question=question, cite=cite, deep=deep, verbose=verbose)) @cli.command("research", help="Run multi-agent research and output a concise report") diff --git a/tests/test_app.py b/tests/test_app.py index cb6a1f65..289f5b95 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -248,6 +248,23 @@ async def test_ask_with_cite(app: HaikuRAGApp, monkeypatch): mock_client.ask.assert_called_once_with("test question", cite=True) +@pytest.mark.asyncio +async def test_ask_with_verbose(app: HaikuRAGApp, monkeypatch): + """Test asking a question with verbose (should be ignored for non-deep).""" + mock_answer = "Test answer" + mock_client = AsyncMock() + mock_client.ask.return_value = mock_answer + mock_client.__aenter__.return_value = mock_client + + mock_print = MagicMock() + monkeypatch.setattr(app.console, "print", mock_print) + + with patch("haiku.rag.app.HaikuRAG", return_value=mock_client): + await app.ask("test question", verbose=True) + + mock_client.ask.assert_called_once_with("test question", cite=False) + + @pytest.mark.asyncio async def test_ask_with_deep(app: HaikuRAGApp, monkeypatch): """Test asking a question with deep QA.""" @@ -308,3 +325,32 @@ async def test_ask_with_deep_and_cite(app: HaikuRAGApp, monkeypatch): call_kwargs = mock_graph.run.call_args[1] assert call_kwargs["state"].context.original_question == "test question" assert call_kwargs["state"].context.use_citations is True + + +@pytest.mark.asyncio +async def test_ask_with_deep_and_verbose(app: HaikuRAGApp, monkeypatch): + """Test asking a question with deep QA and verbose output.""" + from haiku.rag.qa.deep.models import DeepAnswer + + mock_output = DeepAnswer(answer="Deep QA answer", sources=["test.md"]) + mock_result = MagicMock() + mock_result.output = mock_output + + mock_graph = AsyncMock() + mock_graph.run.return_value = mock_result + + mock_client = AsyncMock() + mock_client.__aenter__.return_value = mock_client + + mock_print = MagicMock() + monkeypatch.setattr(app.console, "print", mock_print) + + with patch("haiku.rag.app.HaikuRAG", return_value=mock_client): + with patch( + "haiku.rag.qa.deep.graph.build_deep_qa_graph", return_value=mock_graph + ): + await app.ask("test question", deep=True, verbose=True) + + mock_graph.run.assert_called_once() + call_kwargs = mock_graph.run.call_args[1] + assert call_kwargs["deps"].console is not None diff --git a/tests/test_cli.py b/tests/test_cli.py index 3d70cde6..72aa361e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -207,7 +207,7 @@ def test_ask(): assert result.exit_code == 0 mock_app_instance.ask.assert_called_once_with( - question="What is Python?", cite=False, deep=False + question="What is Python?", cite=False, deep=False, verbose=False ) @@ -221,7 +221,7 @@ def test_ask_with_cite(): assert result.exit_code == 0 mock_app_instance.ask.assert_called_once_with( - question="What is Python?", cite=True, deep=False + question="What is Python?", cite=True, deep=False, verbose=False ) @@ -235,7 +235,7 @@ def test_ask_with_deep(): assert result.exit_code == 0 mock_app_instance.ask.assert_called_once_with( - question="What is Python?", cite=False, deep=True + question="What is Python?", cite=False, deep=True, verbose=False ) @@ -249,7 +249,21 @@ def test_ask_with_deep_and_cite(): assert result.exit_code == 0 mock_app_instance.ask.assert_called_once_with( - question="What is Python?", cite=True, deep=True + question="What is Python?", cite=True, deep=True, verbose=False + ) + + +def test_ask_with_deep_and_verbose(): + with patch("haiku.rag.app.HaikuRAGApp") as mock_app: + mock_app_instance = MagicMock() + mock_app_instance.ask = AsyncMock() + mock_app.return_value = mock_app_instance + + result = runner.invoke(cli, ["ask", "What is Python?", "--deep", "--verbose"]) + + assert result.exit_code == 0 + mock_app_instance.ask.assert_called_once_with( + question="What is Python?", cite=False, deep=True, verbose=True )