# How `source_chat.py` LangGraph Enables Chatting with Specific Data Sources in Open Notebook

> Learn how source_chat.py LangGraph in lfnovo/open-notebook grounds LLM conversations in specific data sources. Discover how it retrieves, prompts, and cleans output for focused insights. Read more now.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: how-to-guide
- Published: 2026-06-06

---

**TLDR:** The [`source_chat.py`](https://github.com/lfnovo/open-notebook/blob/main/source_chat.py) graph in `lfnovo/open-notebook` retrieves a single source’s text and insights, renders them into a dedicated system prompt, provisions an LLM, and cleans the output so every conversation turn stays grounded in that specific document, web page, or video.

The Open Notebook backend is designed to isolate knowledge and let users interrogate it directly. One of its core features is **chatting with specific data sources** through the LangGraph defined in [`open_notebook/graphs/source_chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/source_chat.py). By funneling a single `source_id` through a pipeline of context assembly, prompt rendering, and model invocation, the graph ensures the LLM never drifts from the material it is meant to reference.

## Gathering Source Context with `ContextBuilder`

The graph node `call_model_with_source_context` starts by instantiating `ContextBuilder` from [`open_notebook/utils/context_builder.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/context_builder.py) (lines 59–73). The builder is initialized with the target `source_id` and performs three critical lookups:

- It loads the source record itself.
- It pulls the source’s full text or a shortened representation.
- It fetches any linked `SourceInsight` objects via `Source.get` and `source.get_insights`.

Once the raw pieces are collected, the builder’s internal `_format_response` method (lines 90–108) assembles them into a structured dictionary. This payload includes metadata such as token counts and item counts, giving downstream nodes an accurate picture of how much context is being carried into the chat.

```python
from open_notebook.models.source import Source
from open_notebook.utils.context_builder import ContextBuilder

# Conceptual flow mirrored by ContextBuilder (lines 59-73)

source = Source.get("source-id")
insights = source.get_insights()

# The builder encapsulates the lookups above and produces the

# final payload via _format_response (lines 90-108)

builder = ContextBuilder(source_id="source-id")

```

## Rendering the System Prompt for Source-Aware Chat

The structured context is passed to a `Prompter` using the template key `source_chat/system` (lines 28–30 in [`source_chat.py`](https://github.com/lfnovo/open-notebook/blob/main/source_chat.py)). This Jinja2 template injects the source details, insights, and a formatted context string produced by `_format_source_context` (lines 90–125). The resulting system prompt explicitly tells the LLM which information it may reference and defines its behavioral boundaries for the conversation.

```python
from open_notebook.ai.prompter import Prompter

# Lines 28-30 of open_notebook/graphs/source_chat.py

prompter = Prompter(prompt_template="source_chat/system")

# The prompter loads the Jinja2 template and injects:

# - source metadata

# - linked SourceInsight objects

# - the formatted context block from _format_source_context (lines 90-125)

```

## Provisioning the LLM in a Sync-Friendly Node

Because LangGraph nodes run synchronously, the graph cannot directly `await` the async `provision_langchain_model` helper imported from `open_notebook.ai.provision`. Instead, the node spins up a fresh event loop (lines 33–71) to run the coroutine. This wrapper also respects any per-request `model_override` or model ID supplied through the LangGraph `RunnableConfig`.

```python
import asyncio
from open_notebook.ai.provision import provision_langchain_model

# Lines 33-71: bridge async provisioning into the sync graph node

loop = asyncio.new_event_loop()
try:
    # The call respects model_override or model_id from RunnableConfig

    model = loop.run_until_complete(provision_langchain_model())
finally:
    loop.close()

```

## Invoking the Model and Post-Processing the Response

With the model ready, the node calls `model.invoke(payload)` at line 73 to produce the chat response. The raw output then passes through two cleaning stages before it reaches the user:

- `extract_text_content` strips out any markup tags from the LLM response.
- `clean_thinking_content` removes internal reasoning artifacts or "thinking" sections.

```python

# Line 73: generate the chat response

response = model.invoke(payload)

# Post-processing sanitizes the raw output

cleaned = extract_text_content(response)
final_answer = clean_thinking_content(cleaned)

```

This final sequence guarantees that the user receives a clean, strictly source-grounded reply rather than raw model noise.

## Summary

- [`open_notebook/graphs/source_chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/source_chat.py) defines a LangGraph that isolates a single source for focused conversation.
- The `call_model_with_source_context` node uses `ContextBuilder` (lines 59–73) and `_format_response` (lines 90–108) to assemble source text and insights.
- A `Prompter` with the `source_chat/system` template (lines 28–30) renders a system prompt that anchors the LLM to the source material.
- Async model provisioning is bridged into the sync graph via a new event loop (lines 33–71), honoring `RunnableConfig` overrides.
- `model.invoke(payload)` (line 73) generates the answer, which is then sanitized by `extract_text_content` and `clean_thinking_content`.

## Frequently Asked Questions

### How does [`source_chat.py`](https://github.com/lfnovo/open-notebook/blob/main/source_chat.py) keep the LLM focused on one source?

The graph node `call_model_with_source_context` creates a `ContextBuilder` that loads only the specified `source_id`, its full text, and linked `SourceInsight` objects. This material is rendered into a dedicated system prompt, so the model receives no outside context.

### Why does the graph use a new event loop to provision the model?

`provision_langchain_model` is an async function, but LangGraph nodes execute synchronously. The node wraps the call in `asyncio.new_event_loop()` (lines 33–71) so it can run the coroutine without blocking the main application loop.

### What kind of metadata does `ContextBuilder` include in the context payload?

The builder’s `_format_response` method (lines 90–108 of [`context_builder.py`](https://github.com/lfnovo/open-notebook/blob/main/context_builder.py)) appends metadata such as token counts and item counts, letting the system track exactly how much source material is injected into the prompt.

### How is the raw LLM output cleaned before reaching the user?

After `model.invoke(payload)` (line 73), the response is passed through `extract_text_content` to remove markup tags, and then `clean_thinking_content` to strip internal reasoning artifacts, yielding a clean final answer.