Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions pipeline/core/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def __init__(self, src_dir: Path, build_dir: Path) -> None:
".yaml",
".css",
".js",
".html",
}

# Mapping of language codes to full names for URLs
Expand Down Expand Up @@ -156,8 +157,8 @@ def rewrite_link(match: re.Match) -> str:
url = match.group(2) # The URL
post = match.group(3) # Everything after the URL

# Only rewrite absolute /oss/ paths that don't contain 'images'
if url.startswith("/oss/") and "images" not in url:
# Only rewrite absolute /oss/ paths that don't contain 'images' or 'plugins'
if url.startswith("/oss/") and "images" not in url and "plugins" not in url:
parts = url.split("/")
# Insert full language name after "oss"
parts.insert(2, self.language_url_names[target_language])
Expand Down Expand Up @@ -743,8 +744,11 @@ def is_shared_file(self, file_path: Path) -> bool:
if "snippets" in relative_path.parts:
return True

if "plugins" in relative_path.parts:
return True

# JavaScript and CSS files should be shared (used for custom scripts/styles)
return file_path.suffix.lower() in {".js", ".css"}
return file_path.suffix.lower() in {".js", ".css", ".html", ".json"}

def _copy_shared_files(self) -> None:
"""Copy files that should be shared between versions."""
Expand Down
20 changes: 7 additions & 13 deletions src/oss/langchain/middleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -877,21 +877,15 @@ Build custom middleware by implementing hooks that run at specific points in the

#### Node-style hooks

Run at specific points in the execution flow:
Run at specific points in the execution flow.

:::python
- `before_agent` - Before agent starts (once per invocation)
- `before_model` - Before each model call
- `after_model` - After each model response
- `after_agent` - After agent completes (up to once per invocation)
:::
**Try it:** The interactive widget below shows all available hooks and lets you toggle different combinations to see how they affect the agent execution graph:

:::js
- `beforeAgent` - Before agent starts (once per invocation)
- `beforeModel` - Before each model call
- `afterModel` - After each model response
- `afterAgent` - After agent completes (up to once per invocation)
:::
<iframe
src="/plugins/middleware_visualization/index.html"
style={{ width: "100%", height: "600px", border: "none" }}
title="Interactive Middleware Visualizer"
/>

**Example: Logging middleware**

Expand Down
4 changes: 2 additions & 2 deletions src/oss/langgraph/use-functional-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1475,10 +1475,10 @@ list(graph.get_state_history(config)) # [!code highlight]
```typescript
const config = {
configurable: {
thread_id: "1", # [!code highlight]
thread_id: "1", // [!code highlight]
},
};
const history = []; # [!code highlight]
const history = []; // [!code highlight]
for await (const state of graph.getStateHistory(config)) {
history.push(state);
}
Expand Down
63 changes: 63 additions & 0 deletions src/plugins/middleware_visualization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Middleware Hooks Visualizer

Minimal interactive visualizer for LangChain middleware hooks and agent graphs.

## Features

- **Language-specific diagrams**: Automatically switches between Python (snake_case) and JavaScript (camelCase) naming based on active tab
- **Interactive tooltips**: Hover over checkboxes to see what each hook does
- **Compact layout**: Optimized spacing for better visual hierarchy
- **Mintlify integration**: Uses Mintlify design tokens for consistent styling

## Usage

The `index.html` file can be embedded directly into Mintlify documentation:

```html
<iframe
src="/plugins/middleware_visualization/index.html"
style={{ width: "100%", height: "600px", border: "none" }}
title="Interactive Middleware Visualizer"
/>
```

**Binary naming scheme (5 bits):**
```
Bit 0: tools
Bit 1: before_agent
Bit 2: before_model
Bit 3: after_model
Bit 4: after_agent
```

Example: `10110` = tools + before_model + after_model

## Files

- `index.html` - Embeddable widget with interactive controls and diagram rendering
- `diagrams_python.js` - Python version with snake_case naming (e.g., `before_agent`)
- `diagrams_js.js` - JavaScript version with camelCase naming (e.g., `beforeAgent`)
- `generate_middleware_diagrams.py` - Diagram generator script

## Regenerating Diagrams

To regenerate the diagrams after making changes to middleware hooks:

```bash
uv run python src/plugins/middleware_visualization/generate_middleware_diagrams.py
```

This generates three files:
- `diagrams_python.js` - Python diagrams with snake_case hooks
- `diagrams_js.js` - JavaScript diagrams with camelCase hooks

### Diagram Configuration

The generator creates compact diagrams with:
- `nodeSpacing: 30` - Horizontal spacing between nodes
- `rankSpacing: 40` - Vertical spacing between ranks
- `padding: 10` - Diagram padding

Adjust these values in `generate_middleware_diagrams.py` line 144-150 to modify diagram height.

**Note**: Mintlify's dev server doesn't serve JSON files, so we convert the data to JavaScript files that can be loaded via `<script src>`.
1 change: 1 addition & 0 deletions src/plugins/middleware_visualization/diagrams_js.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/plugins/middleware_visualization/diagrams_python.js

Large diffs are not rendered by default.

256 changes: 256 additions & 0 deletions src/plugins/middleware_visualization/generate_middleware_diagrams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
"""Generate Mermaid diagrams for middleware hook combinations.

Binary naming scheme (5 bits):
- Bit 0: has_tools
- Bit 1: before_agent
- Bit 2: before_model
- Bit 3: after_model
- Bit 4: after_agent

Example: "10110" = tools, before_model, after_model
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from langchain_core.language_models.chat_models import SimpleChatModel
from langchain_core.tools import tool

from langchain.agents.factory import create_agent
from langchain.agents.middleware.types import AgentMiddleware, AgentState


class DemoModel(SimpleChatModel):
"""Demo model for generating diagrams."""

def _call(self, messages, stop=None, run_manager=None, **kwargs):
return "Demo response"

@property
def _llm_type(self) -> str:
return "demo"


@tool
def demo_tool(query: str) -> str:
"""Demo tool for testing."""
return f"Result for: {query}"


class BeforeModelMiddleware(AgentMiddleware):
"""Middleware with only before_model hook."""

def before_model(self, state: AgentState, runtime) -> dict[str, Any] | None:
return None


class AfterModelMiddleware(AgentMiddleware):
"""Middleware with only after_model hook."""

def after_model(self, state: AgentState, runtime) -> dict[str, Any] | None:
return None


class BeforeAgentMiddleware(AgentMiddleware):
"""Middleware with only before_agent hook."""

def before_agent(self, state: AgentState, runtime) -> dict[str, Any] | None:
return None


class AfterAgentMiddleware(AgentMiddleware):
"""Middleware with only after_agent hook."""

def after_agent(self, state: AgentState, runtime) -> dict[str, Any] | None:
return None


def binary_name(
has_tools: bool,
before_agent: bool,
before_model: bool,
after_model: bool,
after_agent: bool,
) -> str:
"""Generate binary name for configuration.

Bit positions:
- Bit 0: has_tools
- Bit 1: before_agent
- Bit 2: before_model
- Bit 3: after_model
- Bit 4: after_agent
"""
bits = [
"1" if has_tools else "0",
"1" if before_agent else "0",
"1" if before_model else "0",
"1" if after_model else "0",
"1" if after_agent else "0",
]
return "".join(bits)


def clean_mermaid_diagram(mermaid: str, language: str = "python") -> str:
"""Clean up Mermaid diagram by simplifying node names and improving styling.

Args:
mermaid: Raw Mermaid diagram from LangGraph.
language: "python" for snake_case or "javascript" for camelCase naming.

Returns:
Cleaned Mermaid diagram with simplified node names.
"""
import re

# Replace middleware class names with simple hook names in node labels
# Pattern matches: node_id(ClassName.hook_name) -> node_id(hook_name)
if language == "javascript":
replacements = [
(r'\(BeforeAgentMiddleware\.before_agent\)', '(beforeAgent)'),
(r'\(BeforeModelMiddleware\.before_model\)', '(beforeModel)'),
(r'\(AfterModelMiddleware\.after_model\)', '(afterModel)'),
(r'\(AfterAgentMiddleware\.after_agent\)', '(afterAgent)'),
# Also update escaped versions in node IDs
(r'BeforeAgentMiddleware\\2ebefore_agent', 'BeforeAgentMiddleware'),
(r'BeforeModelMiddleware\\2ebefore_model', 'BeforeModelMiddleware'),
(r'AfterModelMiddleware\\2eafter_model', 'AfterModelMiddleware'),
(r'AfterAgentMiddleware\\2eafter_agent', 'AfterAgentMiddleware'),
]
else: # python
replacements = [
(r'\(BeforeAgentMiddleware\.before_agent\)', '(before_agent)'),
(r'\(BeforeModelMiddleware\.before_model\)', '(before_model)'),
(r'\(AfterModelMiddleware\.after_model\)', '(after_model)'),
(r'\(AfterAgentMiddleware\.after_agent\)', '(after_agent)'),
]

# Common replacements for both languages
replacements.extend([
# Remove <p> tags from start/end nodes to make them normal size
(r'__start__\(\[<p>__start__</p>\]\)', '__start__([__start__])'),
(r'__end__\(\[<p>__end__</p>\]\)', '__end__([__end__])'),
])

for pattern, replacement in replacements:
mermaid = re.sub(pattern, replacement, mermaid)

# Add more compact layout configuration
# Replace the config section to make diagrams more compact
config_pattern = r'---\nconfig:\n flowchart:\n curve: linear\n---\n'
compact_config = '''---
config:
flowchart:
curve: linear
nodeSpacing: 30
rankSpacing: 40
padding: 10
---
'''
mermaid = re.sub(config_pattern, compact_config, mermaid)

return mermaid


def generate_all_diagrams(language: str = "python") -> dict[str, str]:
"""Generate Mermaid diagrams for all 32 possible hook combinations.

Args:
language: "python" for snake_case or "javascript" for camelCase naming.

Returns:
Dictionary mapping binary configuration names to Mermaid diagram strings.
"""
model = DemoModel()
diagrams = {}

# Generate all 32 combinations (2^5)
for i in range(32):
# Extract bits
has_tools = bool(i & 0b00001)
before_agent = bool(i & 0b00010)
before_model = bool(i & 0b00100)
after_model = bool(i & 0b01000)
after_agent = bool(i & 0b10000)

# Build middleware list
middleware = []
if before_agent:
middleware.append(BeforeAgentMiddleware())
if before_model:
middleware.append(BeforeModelMiddleware())
if after_model:
middleware.append(AfterModelMiddleware())
if after_agent:
middleware.append(AfterAgentMiddleware())

# Generate binary name
name = binary_name(
has_tools,
before_agent,
before_model,
after_model,
after_agent,
)

# Create agent and generate diagram
tools = [demo_tool] if has_tools else []
agent = create_agent(model=model, tools=tools, middleware=middleware)

mermaid = agent.get_graph().draw_mermaid()
# Clean up node names
mermaid = clean_mermaid_diagram(mermaid, language=language)
diagrams[name] = mermaid

print(f"Generated: {name} (tools={int(has_tools)}, "
f"before_agent={int(before_agent)}, before_model={int(before_model)}, "
f"after_model={int(after_model)}, after_agent={int(after_agent)})")

return diagrams


def save_diagrams_to_inline_js(diagrams: dict[str, str], output_path: Path) -> None:
"""Save diagrams as an inline JavaScript constant file.

This is needed because Mintlify's dev server doesn't serve JSON files.
By converting to a JS file, the diagrams data can be loaded via <script src>.

Args:
diagrams: Dictionary mapping binary names to Mermaid diagrams.
output_path: Path where the JS file should be saved.
"""
# Minified JSON (no whitespace)
json_str = json.dumps(diagrams, separators=(',', ':'))
js_content = f'const diagrams = {json_str};'

output_path.write_text(js_content)
print(f"Saved inline JS version to {output_path}")


def main() -> None:
"""Generate all diagrams and save to both JSON and inline JS formats."""
# Generate Python version (snake_case)
print("\nGenerating Python diagrams (snake_case)...")
python_diagrams = generate_all_diagrams(language="python")

# Generate JavaScript version (camelCase)
print("\nGenerating JavaScript diagrams (camelCase)...")
js_diagrams = generate_all_diagrams(language="javascript")

# Save to same directory as script
output_dir = Path(__file__).parent
output_dir.mkdir(parents=True, exist_ok=True)

# Save as inline JS files (for Mintlify to serve)
python_js_path = output_dir / "diagrams_python.js"
save_diagrams_to_inline_js(python_diagrams, python_js_path)

js_js_path = output_dir / "diagrams_js.js"
save_diagrams_to_inline_js(js_diagrams, js_js_path)


if __name__ == "__main__":
main()
Loading
Loading