# How the ask.py Graph Implements Multi-Search Strategy with Vector Retrieval and Synthesis

> Explore how ask.py uses multi search with vector retrieval and synthesis to answer complex questions. Learn about its efficient workflow and LangGraph pipeline.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: deep-dive
- Published: 2026-06-21

---

**The [`ask.py`](https://github.com/lfnovo/open-notebook/blob/main/ask.py) graph in Open Notebook orchestrates a sophisticated multi-search workflow that decomposes a single user question into up to five targeted sub-queries, retrieves relevant content using SurrealDB vector similarity search, and synthesizes a unified answer through a three-stage LangGraph pipeline.**

The Open Notebook repository leverages LangGraph to implement complex AI-driven search workflows. The [`ask.py`](https://github.com/lfnovo/open-notebook/blob/main/ask.py) graph implements a multi-search strategy with vector retrieval and synthesis that automatically expands queries, performs parallel vector searches, and aggregates results into coherent responses.

## Strategy Generation: Decomposing User Questions

The graph begins execution at the `agent` node, defined in `call_model_with_messages` within [`open_notebook/graphs/ask.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/ask.py). This node sends the original user question to a language model using the `ask/entry` prompt template (lines 54-57).

The model returns a structured JSON `Strategy` object (Pydantic model) containing up to five `Search` objects. Each search object includes a specific `term` and `instructions` for targeted retrieval (lines 30-42). This decomposition allows the system to approach complex questions from multiple angles simultaneously.

## Dynamic Sub-Graph Creation and Parallel Execution

The `trigger_queries` function processes the generated strategy and creates dynamic execution paths. For each entry in `strategy.searches`, the function emits a separate `Send` object targeting the `provide_answer` node (lines 83-95).

Each `Send` operation passes the specific `question`, `term`, and `instructions` as part of the graph state. This architecture enables parallel processing of multiple search queries, with each branch executing independently before results are aggregated.

## Vector-Based Retrieval in provide_answer

Inside the `provide_answer` node, the implementation calls the repository-wide `vector_search` utility (line 104). This function, defined in [`open_notebook/domain/notebook.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/notebook.py), generates query embeddings via `generate_embedding` and executes SurrealDB's vector similarity functions to fetch the top-k most relevant chunks across sources and notes (lines 38-64).

The retrieved results include document IDs and raw content, which are packaged into a payload for downstream processing. This vector retrieval step ensures that each sub-query accesses semantically relevant information from the knowledge base.

## Per-Search Synthesis and Context Processing

After retrieving vector results, the graph feeds the payload into a second language model call using the `ask/query_process` prompt template (lines 110-112). This step synthesizes an answer specific to the individual search term and instructions.

The model processes the retrieved documents—referenced by their IDs—and generates a cleaned text response. The implementation appends this synthesized answer to the `answers` list (lines 118-119), which accumulates results from all parallel search branches.

## Final Answer Aggregation

Once all `provide_answer` nodes complete execution, the graph transitions to the `write_final_answer` node. This node receives the original question and the complete list of interim `answers` from the parallel searches.

A third language model call using the `ask/final_answer` prompt template (lines 129-132) composes a unified, polished response that integrates insights from all sub-queries (lines 127-138). This aggregation step ensures the final output is coherent and comprehensive, drawing from multiple retrieval contexts.

## Graph Wiring and State Transitions

The overall state machine defines the execution flow as: `START → agent` → conditional edge to parallel `provide_answer` nodes → `write_final_answer` → `END` (lines 146-155 in [`open_notebook/graphs/ask.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/ask.py)).

This wiring ensures that the strategy generation completes before parallel retrieval begins, and that all retrieval branches finish before final synthesis starts. The LangGraph framework manages these dependencies automatically, handling the fan-out to multiple search queries and the fan-in to the final answer node.

## Code Examples

The following example demonstrates how to execute the ask graph with a specific question:

```python
from open_notebook.graphs.ask import graph

# The question the user asks

question = "What are the privacy guarantees of Open Notebook and how does it store embeddings?"

# Execute the graph – the `configurable` dict can point to specific model IDs

result = await graph.ainvoke(
    {"question": question},
    config={"configurable": {
        "strategy_model": "gpt-4o-mini",
        "answer_model":   "gpt-4o",
        "final_answer_model": "gpt-4o"
    }}
)

print(result["final_answer"])

```

The vector retrieval implementation within `provide_answer` calls the domain utility:

```python

# payload contains term, instructions, etc.

vector_results = await vector_search(state["term"], 10, True, True)
ids = [r["id"] for r in vector_results]          # keep IDs for later reference

payload["results"] = vector_results
payload["ids"] = ids

```

The prompt templates driving the synthesis steps use Jinja2 formatting:

```jinja
{# ask/query_process.jinja #}

You are given:
- Question: {{ question }}
- Retrieved documents ({{ ids|length }} items):
{% for doc in results %}
  • {{ doc.text|truncate(200) }}
{% endfor %}
Please answer the question using only the provided information.

```

```jinja
{# ask/final_answer.jinja #}

You have several partial answers:
{% for a in answers %}
  • {{ a }}
{% endfor %}
Synthesize a single comprehensive answer to the original question.

```

## Summary

- The [`ask.py`](https://github.com/lfnovo/open-notebook/blob/main/ask.py) graph implements a **multi-search strategy** by decomposing questions into up to five parallel sub-queries using the `Strategy` Pydantic model (lines 30-42).
- **Vector retrieval** occurs through `vector_search` in [`open_notebook/domain/notebook.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/notebook.py), which queries SurrealDB using embeddings generated via `generate_embedding` (lines 38-64).
- The `trigger_queries` function (lines 83-95) creates dynamic `Send` objects to execute searches in parallel through the `provide_answer` node.
- **Answer synthesis** happens in two stages: per-query synthesis using the `ask/query_process` prompt (lines 110-112), followed by final aggregation using `ask/final_answer` (lines 129-132).
- The LangGraph state machine manages the workflow from `START` through `agent` to parallel `provide_answer` nodes and finally to `write_final_answer` (lines 146-155).

## Frequently Asked Questions

### How does the ask.py graph handle query decomposition?

The graph uses the `agent` node (`call_model_with_messages`) to send the original question to a language model with the `ask/entry` prompt. The model returns a structured `Strategy` object containing multiple `Search` objects, each with specific terms and instructions. This decomposition allows the system to retrieve information using different angles and keywords simultaneously.

### What database powers the vector retrieval in Open Notebook?

The vector retrieval relies on **SurrealDB**, accessed through the `vector_search` function in [`open_notebook/domain/notebook.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/notebook.py) (lines 38-64). This function generates embeddings using `generate_embedding` and executes SurrealDB's native vector similarity functions to fetch the most relevant content chunks across the knowledge base.

### How does the graph ensure parallel execution of multiple searches?

The `trigger_queries` function (lines 83-95 in [`ask.py`](https://github.com/lfnovo/open-notebook/blob/main/ask.py)) reads the generated strategy and emits separate `Send` objects for each search entry. These `Send` operations target the `provide_answer` node, allowing LangGraph to execute multiple retrieval and synthesis branches in parallel before aggregating results in the final answer node.

### What is the role of the write_final_answer node?

The `write_final_answer` node receives the original question and all interim answers from the parallel search branches. It uses the `ask/final_answer` prompt template (lines 129-132) to call a language model that synthesizes a unified, coherent response from the multiple retrieved contexts, ensuring the final output integrates insights from all sub-queries.