Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
12 changes: 6 additions & 6 deletions .deepagents/skills/docs-code-samples/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Wrap the code that should appear in the docs with snippet tags:

**Python:**
```python
# :snippet-start: snippet-name
# :snippet-start: snippet-name-py
from langchain.tools import tool

@tool
Expand All @@ -78,13 +78,13 @@ def get_weather(city: str) -> str:

**TypeScript/JavaScript:**
```ts
// :snippet-start: snippet-name
// :snippet-start: snippet-name-js
import { tool } from "langchain";
// ... tool definition ...
// :snippet-end:
```

Choose a unique `snippet-name` (kebab-case, e.g. `tool-return-values`). This becomes the output filename.
Choose a unique `snippet-name` in kebab-case. **All snippet names must include a language suffix:** `-py` for Python files and `-js` for TypeScript/JavaScript files (e.g. `tool-return-values-py`, `tool-return-values-js`). This becomes the base of the output filename.

### 3. Add runnable test code in remove blocks

Expand Down Expand Up @@ -186,9 +186,9 @@ Replace the inline code blocks with the snippet components:
| Element | Convention | Example |
|--------|-------------|---------|
| Code file | Descriptive, kebab-case | `return-a-string.py`, `return-a-string.ts` |
| Snippet name | Kebab-case, matches concept | `tool-return-values` |
| MDX snippet (Python) | `{snippet-name}-py.mdx` | `tool-return-values-py.mdx` |
| MDX snippet (JS) | `{snippet-name}-js.mdx` | `tool-return-values-js.mdx` |
| Snippet name | Kebab-case with language suffix: `-py` for Python, `-js` for JS/TS | `tool-return-values-py`, `tool-return-values-js` |
| MDX snippet (Python) | `{snippet-name}.mdx` (snippet name ends in `-py`) | `tool-return-values-py.mdx` |
| MDX snippet (JS) | `{snippet-name}.mdx` (snippet name ends in `-js`) | `tool-return-values-js.mdx` |
| Component name | PascalCase | `ToolReturnValuesPy`, `ToolReturnValuesJs` |

## Script behavior
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies = [
"nbconvert>=7.16.6",
"langchain>=1.0.1",
"langchain-anthropic>=1.0.0",
"langchain-openai>=0.3.0",
]


Expand Down
10 changes: 9 additions & 1 deletion scripts/generate_code_snippet_mdx.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,15 @@ def main() -> None:
content = snippet_file.read_text(encoding="utf-8")
# Create MDX with fenced code block
mdx_content = f"```{language}\n{content.rstrip()}\n```\n"
mdx_path = snippets_dir / f"{snippet_name}.mdx"
# Output filename: tool-return-values-py.mdx from return-a-string.snippet.tool-return-values-py.py
# Snippet names include language suffix (-py, -js); avoid double suffix
snippet_name = ".".join(snippet_file.stem.split(".")[2:])
mdx_filename = (
f"{snippet_name}.mdx"
if snippet_name.endswith(suffix)
else f"{snippet_name}{suffix}.mdx"
)
mdx_path = snippets_dir / mdx_filename
Comment thread
npentrel marked this conversation as resolved.
Outdated
mdx_path.write_text(mdx_content, encoding="utf-8")
print(f"Generated {mdx_path.relative_to(repo_root)}")

Expand Down
76 changes: 76 additions & 0 deletions src/code-samples/langchain/nostream-tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Example of using nostream tag to exclude LLM output from the stream."""

# :snippet-start: nostream-tag-py
from typing import Any, TypedDict, cast

from langchain_anthropic import ChatAnthropic
from langchain_core.messages import BaseMessage
from langgraph.graph import START, StateGraph

# Create two models: one that streams, one that doesn't
streaming_model = ChatAnthropic(
model_name="claude-3-haiku-20240307", timeout=None, stop=None
)
internal_model = ChatAnthropic(
model_name="claude-3-haiku-20240307", timeout=None, stop=None
).with_config({"tags": ["nostream"]})


class State(TypedDict):
"""State for the graph."""

topic: str
public_response: str
internal_analysis: str


def generate_response(state: State) -> dict[str, Any]:
"""Generate a public response that will be streamed."""
topic = state["topic"]
response = streaming_model.invoke(
[{"role": "user", "content": f"Write a short response about {topic}"}]
)
return {"public_response": response.content}


def analyze_internally(state: State) -> dict[str, Any]:
"""Analyze internally without streaming tokens."""
topic = state["topic"]
# This model has the "nostream" tag, so its tokens won't appear in the stream
analysis = internal_model.invoke(
[{"role": "user", "content": f"Analyze the topic: {topic}"}]
)
return {"internal_analysis": analysis.content}


graph = (
StateGraph(State)
.add_node("generate_response", generate_response)
.add_node("analyze_internally", analyze_internally)
.add_edge(START, "generate_response")
.add_edge(START, "analyze_internally")
.compile()
)

initial_state: State = {
"topic": "AI",
"public_response": "",
"internal_analysis": "",
}
stream = graph.stream(cast("Any", initial_state), stream_mode="messages")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should never do this cast in docs, why is this here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

linter was unhappy without it, let me try to remove it


# :remove-start:
# Stream with "messages" mode - only tokens from streaming_model will appear
streamed_nodes: list[str] = []
for msg, metadata in stream:
if isinstance(msg, BaseMessage) and msg.content and isinstance(metadata, dict):
streamed_nodes.append(metadata["langgraph_node"])
# print(msg.content, end="|", flush=True)
assert "analyze_internally" not in streamed_nodes, (
"No tokens from the non-streaming model should appear in the stream"
)

if __name__ == "__main__":
print("\n✓ nostream tag example works as expected") # noqa: T201
Comment thread
sydney-runkle marked this conversation as resolved.
Outdated
# :remove-end:
# :snippet-end:
68 changes: 68 additions & 0 deletions src/code-samples/langchain/nostream-tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Example of using nostream tag to exclude LLM output from the stream.
*/

// :snippet-start: nostream-tag-js
import { ChatAnthropic } from "@langchain/anthropic";
import { StateGraph, StateSchema, START } from "@langchain/langgraph";
import * as z from "zod";

// Create two models: one that streams, one that doesn't
const streamingModel = new ChatAnthropic({ model: "claude-3-haiku-20240307" });
const internalModel = new ChatAnthropic({
model: "claude-3-haiku-20240307",
}).withConfig({
tags: ["nostream"],
});

const State = new StateSchema({
topic: z.string(),
publicResponse: z.string().optional(),
internalAnalysis: z.string().optional(),
});

const generateResponse = async (state: typeof State.State) => {
const topic = state.topic;
// This response will be streamed
const response = await streamingModel.invoke([
{ role: "user", content: `Write a short response about ${topic}` },
]);
return { publicResponse: response.content };
};

const analyzeInternally = async (state: typeof State.State) => {
const topic = state.topic;
// This model has the "nostream" tag, so its tokens won't appear in the stream
const analysis = await internalModel.invoke([
{ role: "user", content: `Analyze the topic: ${topic}` },
]);
return { internalAnalysis: analysis.content };
};

const graph = new StateGraph(State)
.addNode("generateResponse", generateResponse)
.addNode("analyzeInternally", analyzeInternally)
.addEdge(START, "generateResponse")
.addEdge(START, "analyzeInternally")
.compile();

const stream = await graph.stream({ topic: "AI" }, { streamMode: "messages" });
// :snippet-end:

// :remove-start:
// Stream with "messages" mode - only tokens from streamingModel will appear
const streamedNodes: string[] = [];
for await (const [msg, metadata] of stream) {
if (msg.content) {
streamedNodes.push(metadata.langgraph_node);
}
}

if (streamedNodes.includes("analyzeInternally")) {
throw new Error(
"No tokens from the non-streaming model should appear in the stream",
);
}

console.log("\n✓ nostream tag example works as expected");
// :remove-end:
39 changes: 39 additions & 0 deletions src/code-samples/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/code-samples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@langchain/openai": "^1.0.0",
"@langchain/anthropic": "^1.3.22",
"langchain": "^1.2.28",
"zod": "^3.23.0"
Expand Down
26 changes: 24 additions & 2 deletions src/oss/langgraph/streaming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
title: Streaming
---


import NostreamTagPy from '/snippets/code-samples/nostream-tag-py.mdx';
import NostreamTagJs from '/snippets/code-samples/nostream-tag-js.mdx';

LangGraph implements a streaming system to surface real-time updates. Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.

What's possible with LangGraph streaming:

* <Icon icon="share" size={16} /> [**Stream graph state**](#stream-graph-state) — get state updates / values with `updates` and `values` modes.
* <Icon icon="chart-bar" size={16} /> [**Stream subgraph outputs**](#stream-subgraph-outputs) — include outputs from both the parent graph and any nested subgraphs.
* <Icon icon="binary" size={16} /> [**Stream LLM tokens**](#messages) — capture token streams from anywhere: inside nodes, subgraphs, or tools.
* <Icon icon="binary" size={16} /> [**Stream LLM tokens**](#messages) — capture token streams from anywhere: inside nodes, subgraphs, or tools. Use the [nostream tag](#omit-messages-from-the-stream) to omit specific invocations from the stream.
* <Icon icon="table" size={16} /> [**Stream custom data**](#stream-custom-data) — send custom updates or progress signals directly from tool functions.
:::js
* <Icon icon="tool" size={16} /> [**Stream tool progress**](#stream-tool-progress) — track tool lifecycle events (`start`, `progress`, `end`, `error`) in real time.
Expand Down Expand Up @@ -761,6 +762,27 @@ for await (const [msg, metadata] of await graph.stream(
:::
</Accordion>

#### Omit messages from the stream

Use the `nostream` tag to exclude LLM output from the stream entirely. Invocations tagged with `nostream` still run and produce output; their tokens are simply not emitted in `messages` mode.

This is useful when:

- You need LLM output for internal processing (for example structured output) but do not want to stream it to the client
- You stream the same content through a different channel (for example custom UI messages) and want to avoid duplicate output in the `messages` stream

:::python

<NostreamTagPy />

:::

:::js

<NostreamTagJs />

:::

#### Filter by node

To stream tokens only from specific nodes, use `stream_mode="messages"` and filter the outputs by the `langgraph_node` field in the streamed metadata:
Expand Down
46 changes: 46 additions & 0 deletions src/snippets/code-samples/nostream-tag-js.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
```ts
import { ChatAnthropic } from "@langchain/anthropic";
import { StateGraph, StateSchema, START } from "@langchain/langgraph";
import * as z from "zod";

// Create two models: one that streams, one that doesn't
const streamingModel = new ChatAnthropic({ model: "claude-3-haiku-20240307" });
const internalModel = new ChatAnthropic({
model: "claude-3-haiku-20240307",
}).withConfig({
tags: ["nostream"],
});

const State = new StateSchema({
topic: z.string(),
publicResponse: z.string().optional(),
internalAnalysis: z.string().optional(),
});

const generateResponse = async (state: typeof State.State) => {
const topic = state.topic;
// This response will be streamed
const response = await streamingModel.invoke([
{ role: "user", content: `Write a short response about ${topic}` },
]);
return { publicResponse: response.content };
};

const analyzeInternally = async (state: typeof State.State) => {
const topic = state.topic;
// This model has the "nostream" tag, so its tokens won't appear in the stream
const analysis = await internalModel.invoke([
{ role: "user", content: `Analyze the topic: ${topic}` },
]);
return { internalAnalysis: analysis.content };
};

const graph = new StateGraph(State)
.addNode("generateResponse", generateResponse)
.addNode("analyzeInternally", analyzeInternally)
.addEdge(START, "generateResponse")
.addEdge(START, "analyzeInternally")
.compile();

const stream = await graph.stream({ topic: "AI" }, { streamMode: "messages" });
```
Loading
Loading