diff --git a/CHANGELOG.md b/CHANGELOG.md index 04335c3b..e595d314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # Changelog ## [Unreleased] +### Added + +- **Search Filter for Graphs**: Research and Deep QA graphs now support `search_filter` parameter to restrict searches to specific documents + - Set `state.search_filter` to a SQL WHERE clause (e.g., `"id IN ('doc1', 'doc2')"`) before running the graph + - Enables document-scoped research workflows + - CLI: `haiku-rag research "question" --filter "uri LIKE '%paper%'"` + - CLI: `haiku-rag ask "question" --filter "title = 'My Doc'"` + - Python: `client.ask(question, filter="...")` and `agent.answer(question, filter="...")` +- **AG-UI Research Example**: Added bidirectional state demonstration with document filter + - New `/api/documents` endpoint to list available documents + - Frontend document selector component with search and multi-select + - Demonstrates client-to-server state flow via AG-UI protocol + ## [0.20.0] - 2025-12-10 ### Added diff --git a/docs/agents.md b/docs/agents.md index 376ac14d..4d218383 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -229,6 +229,20 @@ async with HaikuRAG(path_to_db) as client: print(report.executive_summary) ``` +### Filtering Documents + +Both Research and Deep QA graphs support restricting searches to specific documents via the `search_filter` parameter. Set it to a SQL WHERE clause before running: + +```python +state = ResearchState.from_config(context=context, config=Config) +# Only search documents with these IDs +state.search_filter = "id IN ('doc-123', 'doc-456')" + +result = await graph.run(state=state, deps=deps) +``` + +The filter applies to all search operations in the graph (context gathering and sub-question searches). See [Filtering Search Results](python.md#filtering-search-results) for available filter columns and syntax. + Alternative usage with custom config: ```python diff --git a/docs/cli.md b/docs/cli.md index 1d432f7d..3e0edf0f 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -149,8 +149,19 @@ Show verbose output with deep QA: 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. +Filter to specific documents: +```bash +haiku-rag ask "What are the main findings?" --filter "uri LIKE '%paper%'" +``` + +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: + +- `--cite`: Include citations showing which documents were used +- `--deep`: Decompose the question into sub-questions answered in parallel before synthesizing a final answer +- `--verbose`: Show planning, searching, evaluation, and synthesis steps (only with `--deep`) +- `--filter`: Restrict searches to documents matching the filter (see [Filtering Search Results](python.md#filtering-search-results)) ## Research @@ -166,8 +177,16 @@ With verbose output to see progress: haiku-rag research "How does haiku.rag organize and query documents?" --verbose ``` +Filter to specific documents: + +```bash +haiku-rag research "What are the key findings?" --filter "uri LIKE '%paper%'" +``` + Flags: + - `--verbose`: Show planning, searching previews, evaluation summary, and stop reason +- `--filter`: SQL WHERE clause to filter documents (see [Filtering Search Results](python.md#filtering-search-results)) Research parameters like `max_iterations`, `confidence_threshold`, and `max_concurrency` are configured in your [configuration file](configuration/index.md) under the `research` section. diff --git a/examples/ag-ui-research/backend/agent.py b/examples/ag-ui-research/backend/agent.py index 0182f1e6..3dce81ac 100644 --- a/examples/ag-ui-research/backend/agent.py +++ b/examples/ag-ui-research/backend/agent.py @@ -33,6 +33,7 @@ class AgentDeps: client: HaikuRAG agui_emitter: "AGUIEmitter[ResearchState, ResearchReport] | None" = None + search_filter: str | None = None model = get_model(Config.research.model, Config) @@ -74,6 +75,7 @@ async def run_research(ctx: RunContext[AgentDeps], question: str) -> str: graph = build_research_graph(Config) context = ResearchContext(original_question=question) state = ResearchState.from_config(context=context, config=Config) + state.search_filter = ctx.deps.search_filter graph_deps = ResearchDeps( client=ctx.deps.client, diff --git a/examples/ag-ui-research/backend/main.py b/examples/ag-ui-research/backend/main.py index 32a2fec0..d432a8ba 100644 --- a/examples/ag-ui-research/backend/main.py +++ b/examples/ag-ui-research/backend/main.py @@ -92,8 +92,19 @@ async def stream_research_agent(request: Request) -> StreamingResponse: effective_db_path = Path(effective_db_path) client = get_client(effective_db_path) + # Build search filter from document IDs (empty list = search all) + document_ids = input_data.state.get("documentFilter") or [] + search_filter = None + if document_ids: + ids_str = ", ".join(f"'{id}'" for id in document_ids) + search_filter = f"id IN ({ids_str})" + # Create agent dependencies with shared emitter - agent_deps = AgentDeps(client=client, agui_emitter=emitter) + agent_deps = AgentDeps( + client=client, + agui_emitter=emitter, + search_filter=search_filter, + ) # Start run with empty initial state emitter.start_run( @@ -189,6 +200,19 @@ async def health_check(_: Request) -> JSONResponse: ) +async def list_documents(_: Request) -> JSONResponse: + """List all documents in the database.""" + client = get_client(db_path) + docs = await client.document_repository.list_all() + return JSONResponse( + { + "documents": [ + {"id": doc.id, "title": doc.title, "uri": doc.uri} for doc in docs + ] + } + ) + + async def visualize_chunk(request: Request) -> JSONResponse: """Return visual grounding images for a chunk as base64.""" import base64 @@ -228,6 +252,7 @@ async def visualize_chunk(request: Request) -> JSONResponse: app = Starlette( routes=[ Route("/v1/research/stream", stream_research_agent, methods=["POST"]), + Route("/api/documents", list_documents, methods=["GET"]), Route("/api/visualize/{chunk_id}", visualize_chunk, methods=["GET"]), Route("/health", health_check, methods=["GET"]), ], diff --git a/examples/ag-ui-research/frontend/components/Agent.tsx b/examples/ag-ui-research/frontend/components/Agent.tsx index ac297cfc..a8dc5f59 100644 --- a/examples/ag-ui-research/frontend/components/Agent.tsx +++ b/examples/ag-ui-research/frontend/components/Agent.tsx @@ -3,6 +3,7 @@ import { CopilotKit, useCoAgent } from "@copilotkit/react-core"; import { CopilotChat } from "@copilotkit/react-ui"; import "@copilotkit/react-ui/styles.css"; +import DocumentSelector from "./DocumentSelector"; import StateDisplay from "./StateDisplay"; interface InsightRecord { @@ -82,10 +83,11 @@ interface ResearchState { result?: ResearchReport; current_activity?: string; current_activity_message?: string; + documentFilter?: string[]; } function AgentContent() { - const { state } = useCoAgent({ + const { state, setState, running } = useCoAgent({ name: "research_agent", initialState: { context: { @@ -101,9 +103,14 @@ function AgentContent() { max_concurrency: 1, last_eval: null, last_analysis: null, + documentFilter: [], }, }); + const handleDocumentFilterChange = (ids: string[]) => { + setState({ ...state, documentFilter: ids }); + }; + return ( <>