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

The 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 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. 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, 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_answerEND (lines 146-155 in 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:

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:


# 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:

{# 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.
{# 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 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, 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 (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) 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →