Understanding the ask.py Graph Flow: Multi-Search Strategy and Answer Synthesis in Open Notebook
The ask.py graph implements a LangGraph-powered workflow that dynamically generates search strategies, executes parallel vector searches, and synthesizes intermediate results into a coherent final answer.
The ask.py module in the lfnovo/open-notebook repository orchestrates a sophisticated multi-search workflow using LangGraph's StateGraph. This system enables the LLM to first devise a search strategy, then execute multiple independent vector lookups, and finally combine the results into a single synthesized response. Understanding this graph flow is essential for developers looking to customize the question-answering pipeline or debug the orchestration logic.
Core Graph Architecture
The graph consists of four specialized nodes defined in open_notebook/graphs/ask.py that transform a user question into a final answer through a planned, multi-step retrieval process.
Strategy Generation Node (agent)
The agent node (lines 51-75) serves as the entry point where the LLM generates a structured search plan. The call_model_with_messages function reads the user prompt, renders the ask/entry system prompt, and calls a provisioned LLM to produce a JSON output. This output is parsed into a Strategy Pydantic model containing a list of Search items, each defined by a term and specific instructions.
Dynamic Query Dispatch (trigger_queries)
Rather than hard-coding parallel branches, the trigger_queries function (lines 83-95) creates a dynamic list of Send-type edges at runtime. For every Search object in state["strategy"].searches, it dispatches one provide_answer node. This architecture supports up to five concurrent searches without modifying the graph structure, allowing the LLM to decide exactly how many queries are needed based on the question complexity.
Per-Search Answer Generation (provide_answer)
Each dispatched provide_answer node (lines 98-124) executes a self-contained retrieval and reasoning cycle. The node first calls vector_search (implemented in open_notebook/domain/notebook.py) to perform a similarity lookup on the search term. It then builds the ask/query_process prompt with the retrieved document IDs, sends this to the answer LLM, and returns a cleaned intermediate answer that gets accumulated in state["answers"].
Final Answer Synthesis (write_final_answer)
The write_final_answer node (lines 126-144) aggregates all intermediate answers from the parallel search branches. It renders the ask/final_answer prompt with the complete context, invokes the final-answer LLM, and produces the user-facing response. This node only executes after all provide_answer instances have completed, ensuring the synthesis has access to the full information spectrum.
Step-by-Step Execution Flow
The graph follows a deterministic sequence from invocation to completion:
-
Graph Entry — The workflow starts at
START → agentwith an initial state containing the userquestion. -
Strategy Formulation — The
agentnode generates a JSON strategy describing the search approach (e.g., splitting a complex query into architectural and policy components). -
Dynamic Dispatch — The
trigger_queriesfunction inspectsstate["strategy"]and creates parallelSend("provide_answer", ...)edges for each search term. -
Parallel Retrieval — Multiple
provide_answerinstances run concurrently, each performing vector lookups in SurrealDB and generating sub-answers via theask/query_processprompt. -
Aggregation and Synthesis — Once all branches complete,
write_final_answercombines the results using theask/final_answerprompt to produce a coherent final response. -
Completion — The graph reaches
END(lines 146-155), returningfinal_answerto the caller.
Implementation Code Examples
Invoking the Graph from Python
You can execute the graph asynchronously using the compiled StateGraph object:
from open_notebook.graphs.ask import graph
# Initialize with the user question
payload = {"question": "What are the privacy features of Open Notebook?"}
# Execute the workflow (typically via FastAPI)
final_output = await graph.ainvoke(payload, config={})
print(final_output["final_answer"])
The graph object is instantiated at the bottom of ask.py (lines 146-155) and represents the complete compiled workflow.
Strategy JSON Structure
When the LLM processes the ask/entry prompt, it returns a structured JSON object that maps to the Strategy model:
{
"reasoning": "The user asks about privacy; we need both architectural and policy info.",
"searches": [
{
"term": "privacy architecture",
"instructions": "Summarize the high-level design and data-flow controls."
},
{
"term": "privacy policy",
"instructions": "List the key privacy statements from the repository."
}
]
}
These Search objects drive the dynamic dispatch mechanism, with each item spawning an independent provide_answer node.
Configuring LLM Models
The graph supports different models for each phase via the configurable parameter:
config = {
"configurable": {
"strategy_model": "gpt-4o-mini", # For initial search planning
"answer_model": "gpt-4o-mini", # For each sub-search answer
"final_answer_model": "gpt-4o" # For the final synthesis
}
}
final_output = await graph.ainvoke(payload, config=config)
The provision_langchain_model helper (defined in open_notebook/ai/provision.py) reads these configuration values to instantiate the appropriate LangChain wrappers for each node.
Key Implementation Files
The multi-search strategy relies on several supporting modules:
open_notebook/graphs/ask.py— Core graph definition, node implementations, and LangGraph wiring (lines 146-155).open_notebook/ai/provision.py— Supplies provisioned LLMs viaprovision_langchain_modelused by all graph nodes.open_notebook/domain/notebook.py— Implementsvector_searchfor retrieving embeddings from SurrealDB.ai_prompter/prompter.py— Renders the three prompt templates:ask/entry,ask/query_process, andask/final_answer.open_notebook/utils/*.py— Utility functions likeclean_thinking_contentandextract_text_contentfor sanitizing LLM outputs.
Summary
- The ask.py graph flow uses LangGraph to orchestrate a multi-step retrieval and synthesis pipeline.
- The agent node generates a dynamic
Strategythat determines which searches to execute. - Dynamic dispatch via
Sendedges allows parallel execution of up to five independent vector searches. - Each provide_answer node performs a targeted vector lookup and generates an intermediate answer.
- The write_final_answer node synthesizes all intermediate results into a final coherent response using configurable LLM models.
Frequently Asked Questions
How does the graph handle multiple search queries without hard-coded branches?
The trigger_queries function (lines 83-95) creates dynamic Send edges at runtime based on the Strategy model output. This allows the graph to spawn exactly as many provide_answer nodes as needed (up to five) without modifying the underlying graph structure, providing flexibility for complex questions requiring multiple retrieval angles.
What determines which LLM model is used for answer synthesis?
The provision_langchain_model helper reads from the configurable dictionary in the graph config. You can specify strategy_model, answer_model, and final_answer_model independently, allowing cost-effective models for planning and retrieval while using more capable models for the final synthesis step.
Where is the vector search actually implemented?
The vector_search function is defined in open_notebook/domain/notebook.py. The provide_answer node calls this function to perform similarity searches against SurrealDB embeddings, then passes the retrieved document IDs to the LLM via the ask/query_process prompt template.
Can the graph execution be customized or extended?
Yes, the modular node structure in ask.py allows developers to override specific behaviors. You can modify the prompt templates in ai_prompter/prompter.py, swap the vector search implementation, or adjust the Strategy Pydantic model to include additional metadata fields that influence downstream node behavior.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →