75601988c2
- Cover, TOC, 5 dividers, 3 recap slides - 5 sections (chains, langgraph, deepagents, openswe, ecosystem) - design-system.js with theme tokens + 9 helper functions - research/: timeline + sources + per-tech notes - final-compile.js + merge.js for rebuild pipeline - output/: langchain-evolution.pptx (2.3 MB) + langchain-evolution.pdf (1.1 MB) + 7 sample previews
1530 lines
54 KiB
JavaScript
1530 lines
54 KiB
JavaScript
/**
|
||
* compile.js
|
||
* ----------------------------------------------------------------------------
|
||
* Section 2: LangGraph 1.0 -- state, nodes, persistence, HITL.
|
||
* 26 slides, 16:9, dark theme, code-heavy.
|
||
*
|
||
* Output: section2.pptx (same dir)
|
||
* PDF + PNG preview: produced by build.sh (soffice + pdftoppm).
|
||
*
|
||
* Run: node compile.js
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const path = require('path');
|
||
const ds = require(path.join(__dirname, '..', '..', 'design-system.js'));
|
||
const { theme, helpers, layouts } = ds;
|
||
const { slideBase, addHeader, addCodeBlock, addCallout, addProsCons,
|
||
addPageNumber, addSectionDivider, addSourceLine, withFallback } = helpers;
|
||
|
||
const pptxgen = require('pptxgenjs');
|
||
|
||
const pres = new pptxgen();
|
||
pres.layout = 'LAYOUT_16x9';
|
||
pres.title = 'LangChain Evolution: Section 2 -- LangGraph 1.0';
|
||
pres.subject = 'LangGraph 1.0 state, nodes, persistence, HITL';
|
||
|
||
// Override code font size for this section: more lines fit per card.
|
||
// Original sizes.code = 12 (from design-system). 10 fits ~35% more.
|
||
const CODE_FONT_SIZE = 10;
|
||
theme.sizes.code = CODE_FONT_SIZE;
|
||
|
||
// Section-wide constants
|
||
const SECTION_NUMBER = 2;
|
||
const SECTION_LABEL = 'SECTION 2';
|
||
|
||
// Slide counter for page numbers (1-based)
|
||
let n = 0;
|
||
const next = () => ++n;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 1 -- Section divider
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
addSectionDivider(s, pres, theme, {
|
||
number: SECTION_NUMBER,
|
||
eyebrow: SECTION_LABEL + ': LANGGRAPH 1.0',
|
||
title: 'LangGraph 1.0: stateful runtime',
|
||
intro: 'State, nodes, persistence, HITL. ' +
|
||
'Production-ready durable execution: ' +
|
||
'агенты, которые переживают падение сервера и одобряются человеком.',
|
||
});
|
||
// No page number on divider -- standard practice.
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 2 -- Why LangGraph
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'WHY LANGGRAPH',
|
||
title: 'Зачем LangGraph: что не может обычный LangChain',
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 4.5, h: 3.5,
|
||
kind: 'info',
|
||
title: 'LCEL хорош для',
|
||
text: '- простых pipeline (prompt | model | parser)\n' +
|
||
'- одного прохода без ветвлений\n' +
|
||
'- read-only чатов без state между вызовами',
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 5.2, y: layouts.CONTENT_TOP, w: 4.3, h: 3.5,
|
||
kind: 'warning',
|
||
title: 'LCEL не даёт',
|
||
text: '- циклов и произвольной топологии графа\n' +
|
||
'- first-class persistence и time-travel\n' +
|
||
'- настоящей паузы на human approval\n' +
|
||
'- multi-agent и параллельных веток (Send)\n' +
|
||
'- восстановления после падения посреди long-run',
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'blog.langchain.com/langchain-langgraph-1dot0',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 3 -- Install
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'INSTALL',
|
||
title: 'Установка: одна команда',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 6.0, h: 1.3,
|
||
code: 'pip install -U langgraph',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: 2.85, w: 6.0, h: 2.0,
|
||
code: [
|
||
'# extras: postgres / sqlite checkpointers -- отдельные пакеты',
|
||
'pip install -U langgraph langgraph-checkpoint-postgres',
|
||
'pip install -U langgraph langgraph-checkpoint-sqlite',
|
||
'',
|
||
'# JS / TypeScript',
|
||
'npm install @langchain/langgraph @langchain/langgraph-checkpoint',
|
||
].join('\n'),
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 6.8, y: layouts.CONTENT_TOP, w: 2.7, h: 3.5,
|
||
kind: 'success',
|
||
title: 'Что в коробке',
|
||
text: 'state, persistence, HITL, streaming, ToolNode, subgraph API. ' +
|
||
'Никаких внешних сервисов кроме самого рантайма.',
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'pypi.org/project/langgraph (langgraph 1.2.6, 2026-06-18)',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 4 -- State: TypedDict basic
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'STATE / 1',
|
||
title: 'State как TypedDict -- первая итерация',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.5,
|
||
code: [
|
||
'from typing_extensions import TypedDict',
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'',
|
||
'class State(TypedDict):',
|
||
' question: str',
|
||
' answer: str',
|
||
' steps: int',
|
||
'',
|
||
'def answer(state: State) -> dict:',
|
||
' return {"answer": f"echo: {state[\'question\']}", "steps": 1}',
|
||
'',
|
||
'builder = StateGraph(State)',
|
||
'builder.add_node("answer", answer)',
|
||
'builder.add_edge(START, "answer")',
|
||
'builder.add_edge("answer", END)',
|
||
'graph = builder.compile()',
|
||
'',
|
||
'print(graph.invoke({"question": "hi", "steps": 0}))',
|
||
'# -> {\'question\': \'hi\', \'answer\': \'echo: hi\', \'steps\': 1}',
|
||
].join('\n'),
|
||
highlightLines: [4, 5, 6, 17, 18, 19, 21],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/low_level/',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 5 -- State: Annotated + add_messages reducer
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'STATE / 2',
|
||
title: 'Annotated + add_messages -- каналы с reducer',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'from typing import Annotated',
|
||
'from typing_extensions import TypedDict',
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'from langgraph.graph.message import add_messages',
|
||
'',
|
||
'class State(TypedDict):',
|
||
' # reducer add_messages склеивает списки сообщений по правилам',
|
||
' messages: Annotated[list, add_messages]',
|
||
'',
|
||
'def echo(state: State):',
|
||
' last = state["messages"][-1]',
|
||
' return {"messages": [{"role": "assistant", "content": f"echo: {last.content}"}]}',
|
||
'',
|
||
'g = StateGraph(State)',
|
||
'g.add_node("echo", echo)',
|
||
'g.add_edge(START, "echo")',
|
||
'g.add_edge("echo", END)',
|
||
'app = g.compile()',
|
||
'',
|
||
'print(app.invoke({"messages": [{"role": "user", "content": "hi"}]}))',
|
||
].join('\n'),
|
||
highlightLines: [8, 12, 13],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/low_level/#reducers',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 6 -- State: operator.add
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'STATE / 3',
|
||
title: 'operator.add -- аккумуляция для list-канала',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'import operator',
|
||
'from typing import Annotated',
|
||
'from typing_extensions import TypedDict',
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'',
|
||
'class State(TypedDict):',
|
||
' # каждый узел возвращает кусок списка, operator.add склеивает',
|
||
' log: Annotated[list[str], operator.add]',
|
||
' tokens: Annotated[int, operator.add]',
|
||
'',
|
||
'def step(state: State):',
|
||
' return {"log": ["step-1"], "tokens": 12}',
|
||
'',
|
||
'def another(state: State):',
|
||
' return {"log": ["step-2"], "tokens": 7}',
|
||
'',
|
||
'g = StateGraph(State)',
|
||
'g.add_node("a", step)',
|
||
'g.add_node("b", another)',
|
||
'g.add_edge(START, "a")',
|
||
'g.add_edge("a", "b")',
|
||
'g.add_edge("b", END)',
|
||
'app = g.compile()',
|
||
'print(app.invoke({"log": [], "tokens": 0}))',
|
||
'# -> {\'log\': [\'step-1\', \'step-2\'], \'tokens\': 19}',
|
||
].join('\n'),
|
||
highlightLines: [9, 10],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/low_level/#reducers',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 7 -- State: dataclass + LastValue
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'STATE / 4',
|
||
title: 'dataclass + LastValue -- когда хочется типизации',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'from dataclasses import dataclass, field',
|
||
'from typing import Annotated',
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'from langgraph.graph.message import add_messages',
|
||
'',
|
||
'@dataclass',
|
||
'class State:',
|
||
' question: str = ""',
|
||
' # dataclass + Annotated -- каналы работают точно так же',
|
||
' messages: Annotated[list, add_messages] = field(default_factory=list)',
|
||
' approved: bool = False',
|
||
'',
|
||
'def greet(state: State):',
|
||
' return {"messages": [{"role": "assistant", "content": f"hi, {state.question}"}]}',
|
||
'',
|
||
'g = StateGraph(State)',
|
||
'g.add_node("greet", greet)',
|
||
'g.add_edge(START, "greet")',
|
||
'g.add_edge("greet", END)',
|
||
'app = g.compile()',
|
||
'print(app.invoke(State(question="alex")))',
|
||
].join('\n'),
|
||
highlightLines: [9, 10],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/low_level/#dataclass',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 8 -- StateGraph: builder.compile() минимальный граф
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'STATEGRAPH / 1',
|
||
title: 'StateGraph: builder.compile() -- базовый граф',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'from typing_extensions import TypedDict',
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'',
|
||
'class State(TypedDict):',
|
||
' n: int',
|
||
'',
|
||
'def inc(state: State):',
|
||
' return {"n": state["n"] + 1}',
|
||
'',
|
||
'builder = StateGraph(State)',
|
||
'builder.add_node("inc", inc) # регистрируем узел',
|
||
'builder.add_edge(START, "inc") # поток входа',
|
||
'builder.add_edge("inc", END) # поток выхода',
|
||
'',
|
||
'graph = builder.compile() # компиляция -- граф готов к invoke',
|
||
'',
|
||
'print(graph.invoke({"n": 0})) # -> {\'n\': 1}',
|
||
].join('\n'),
|
||
highlightLines: [12, 13, 14, 17, 19],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/low_level/#stategraph',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 9 -- START, END and data flow
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'STATEGRAPH / 2',
|
||
title: 'START, END и поток данных через рёбра',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'# START и END -- это специальные sentinel-узлы, не callable',
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'',
|
||
'class State(TypedDict):',
|
||
' text: str',
|
||
'',
|
||
'def upper(state: State):',
|
||
' return {"text": state["text"].upper()}',
|
||
'',
|
||
'def exclaim(state: State):',
|
||
' return {"text": state["text"] + "!"}',
|
||
'',
|
||
'g = StateGraph(State)',
|
||
'g.add_node("upper", upper)',
|
||
'g.add_node("exclaim", exclaim)',
|
||
'g.add_edge(START, "upper") # вход в граф',
|
||
'g.add_edge("upper", "exclaim") # внутреннее ребро',
|
||
'g.add_edge("exclaim", END) # выход из графа',
|
||
'',
|
||
'app = g.compile()',
|
||
'print(app.invoke({"text": "hi"})) # -> {\'text\': \'HI!\'}',
|
||
].join('\n'),
|
||
highlightLines: [3, 14, 16, 17],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/low_level/#why-langgraph',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 10 -- Nodes: sync/async
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'NODES / 1',
|
||
title: 'Узлы: синхронные и асинхронные функции',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'import asyncio',
|
||
'from typing_extensions import TypedDict',
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'',
|
||
'class State(TypedDict):',
|
||
' out: str',
|
||
'',
|
||
'def sync_node(state: State): # обычная функция',
|
||
' return {"out": "sync-ok"}',
|
||
'',
|
||
'async def async_node(state: State): # async тоже работает',
|
||
' await asyncio.sleep(0)',
|
||
' return {"out": "async-ok"}',
|
||
'',
|
||
'g = StateGraph(State)',
|
||
'g.add_node("s", sync_node)',
|
||
'g.add_node("a", async_node)',
|
||
'g.add_edge(START, "s")',
|
||
'g.add_edge("s", "a")',
|
||
'g.add_edge("a", END)',
|
||
'app = g.compile()',
|
||
'',
|
||
'print(app.invoke({"out": ""}))',
|
||
'print(asyncio.run(app.ainvoke({"out": ""})))',
|
||
].join('\n'),
|
||
highlightLines: [11, 13],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/low_level/#nodes',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 11 -- Command: explicit goto
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'NODES / 2',
|
||
title: 'Command -- узел сам решает, куда идти дальше',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'from langgraph.types import Command',
|
||
'from typing_extensions import TypedDict',
|
||
'from typing import Literal',
|
||
'class State(TypedDict):',
|
||
' n: int',
|
||
'def decide(state: State) -> Command[Literal["inc", "halt"]]:',
|
||
' # Command(update, goto) -- обновляет state и сам выбирает узел',
|
||
' if state["n"] < 3:',
|
||
' return Command(update={"n": state["n"] + 1}, goto="inc")',
|
||
' return Command(update={}, goto="halt")',
|
||
'g = StateGraph(State)',
|
||
'g.add_node("decide", decide)',
|
||
'g.add_node("inc", inc)',
|
||
'g.add_node("halt", lambda s: s)',
|
||
'g.add_edge(START, "decide")',
|
||
'g.add_edge("inc", "decide")',
|
||
'g.add_edge("halt", END)',
|
||
'app = g.compile()',
|
||
'print(app.invoke({"n": 0}))',
|
||
].join('\n'),
|
||
highlightLines: [7, 8, 10],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/low_level/#command',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 12 -- Conditional edges
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'ROUTING / 1',
|
||
title: 'Conditional edges: routing по содержимому',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'from typing_extensions import TypedDict',
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'',
|
||
'class State(TypedDict):',
|
||
' needs_tool: bool',
|
||
' answer: str',
|
||
'',
|
||
'def agent(state: State) -> dict:',
|
||
' return {"answer": "model-output"}',
|
||
'',
|
||
'def tool_node(state: State) -> dict:',
|
||
' return {"answer": "tool-output"}',
|
||
'',
|
||
'def route(state: State) -> str:',
|
||
' # возвращаем ключ, который есть в path_map ниже',
|
||
' return "tool_node" if state["needs_tool"] else END',
|
||
'',
|
||
'g = StateGraph(State)',
|
||
'g.add_node("agent", agent)',
|
||
'g.add_node("tool_node", tool_node)',
|
||
'g.add_edge(START, "agent")',
|
||
'g.add_conditional_edges("agent", route, {',
|
||
' "tool_node": "tool_node",',
|
||
' END: END,',
|
||
'})',
|
||
'g.add_edge("tool_node", END)',
|
||
'app = g.compile()',
|
||
'print(app.invoke({"needs_tool": True, "answer": ""}))',
|
||
].join('\n'),
|
||
highlightLines: [19, 20, 21, 22, 23],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/low_level/#conditional-edges',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 13 -- Cycles
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'ROUTING / 2',
|
||
title: 'Циклы: agentic loop без рекурсии в коде',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'# агентный цикл: agent <-> tools, выход когда done=true',
|
||
'from typing_extensions import TypedDict',
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'',
|
||
'class State(TypedDict):',
|
||
' done: bool',
|
||
' iter: int',
|
||
'',
|
||
'def agent(state: State) -> dict:',
|
||
' return {"iter": state["iter"] + 1, "done": state["iter"] >= 3}',
|
||
'',
|
||
'def maybe_continue(state: State) -> str:',
|
||
' return "agent" if not state["done"] else END',
|
||
'',
|
||
'g = StateGraph(State)',
|
||
'g.add_node("agent", agent)',
|
||
'g.add_edge(START, "agent")',
|
||
'g.add_conditional_edges("agent", maybe_continue, {"agent": "agent", END: END})',
|
||
'app = g.compile()',
|
||
'print(app.invoke({"done": False, "iter": 0}))',
|
||
'# agent крутится 3 раза, потом уходит в END',
|
||
].join('\n'),
|
||
highlightLines: [15, 16],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/low_level/#cycles',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 14 -- Persistence: InMemorySaver + thread_id
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'PERSISTENCE / 1',
|
||
title: 'InMemorySaver + thread_id: stateful сессии',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'from typing import Annotated',
|
||
'from typing_extensions import TypedDict',
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'from langgraph.graph.message import add_messages',
|
||
'from langgraph.checkpoint.memory import InMemorySaver',
|
||
'',
|
||
'class State(TypedDict):',
|
||
' messages: Annotated[list, add_messages]',
|
||
'',
|
||
'def echo(state: State):',
|
||
' last = state["messages"][-1]',
|
||
' return {"messages": [{"role": "assistant", "content": f"echo: {last.content}"}]}',
|
||
'',
|
||
'g = StateGraph(State)',
|
||
'g.add_node("echo", echo)',
|
||
'g.add_edge(START, "echo")',
|
||
'g.add_edge("echo", END)',
|
||
'',
|
||
'# checkpointer -- обязателен для thread persistence',
|
||
'checkpointer = InMemorySaver()',
|
||
'app = g.compile(checkpointer=checkpointer)',
|
||
'',
|
||
'cfg = {"configurable": {"thread_id": "user-1"}}',
|
||
'app.invoke({"messages": [{"role": "user", "content": "hi"}]}, cfg)',
|
||
'app.invoke({"messages": [{"role": "user", "content": "again"}]}, cfg)',
|
||
'# второй вызов видит все сообщения первого: thread persistence работает',
|
||
].join('\n'),
|
||
highlightLines: [22, 23, 25, 26],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/persistence/',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 15 -- SqliteSaver / PostgresSaver
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'PERSISTENCE / 2',
|
||
title: 'SqliteSaver и PostgresSaver для production',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 4.5, h: 3.65,
|
||
code: [
|
||
'# SQLite -- файл на диске, идеален для dev / small-prod',
|
||
'from langgraph.checkpoint.sqlite import SqliteSaver',
|
||
'',
|
||
'with SqliteSaver.from_conn_string("./checkpoints.db") as cp:',
|
||
' app = g.compile(checkpointer=cp)',
|
||
' cfg = {"configurable": {"thread_id": "u1"}}',
|
||
' app.invoke({"messages": []}, cfg)',
|
||
'',
|
||
'# данные переживают рестарт процесса.',
|
||
'# Для asyncio-варианта -- aiosqlite.',
|
||
].join('\n'),
|
||
highlightLines: [1, 3, 4, 5],
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 5.2, y: layouts.CONTENT_TOP, w: 4.3, h: 3.65,
|
||
code: [
|
||
'# Postgres -- production-grade, multi-instance',
|
||
'from langgraph.checkpoint.postgres import PostgresSaver',
|
||
'',
|
||
'DB = "postgresql://user:pass@host:5432/lg"',
|
||
'with PostgresSaver.from_conn_string(DB) as cp:',
|
||
' # первый запуск создаст schema',
|
||
' cp.setup()',
|
||
' app = g.compile(checkpointer=cp)',
|
||
' cfg = {"configurable": {"thread_id": "u1"}}',
|
||
' app.invoke({"messages": []}, cfg)',
|
||
].join('\n'),
|
||
highlightLines: [1, 3, 6],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/persistence/#checkpointer-implementations',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 16 -- StateSnapshot
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'PERSISTENCE / 3',
|
||
title: 'StateSnapshot -- что лежит в checkpoint',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'# После invoke можно достать текущий snapshot:',
|
||
'snapshot = app.get_state(cfg)',
|
||
'',
|
||
'# snapshot -- это StateSnapshot с полями:',
|
||
'# .config -- thread_id + checkpoint_id',
|
||
'# .metadata -- step, source, writes',
|
||
'# .values -- текущие значения всех каналов state',
|
||
'# .next -- tuple узлов, которые будут выполняться следующими',
|
||
'# .tasks -- PregelTask с pending/result/error',
|
||
'',
|
||
'print(snapshot.next) # () -- граф завершён',
|
||
'print(snapshot.values["messages"][-1].content)',
|
||
].join('\n'),
|
||
highlightLines: [2, 5, 6, 7, 8, 9],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/persistence/#state-snapshot',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 17 -- Time travel: get_state_history
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'TIME TRAVEL / 1',
|
||
title: 'get_state_history: вся история шагов',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'# Каждый super-step -- отдельный checkpoint в thread',
|
||
'history = list(app.get_state_history(cfg))',
|
||
'',
|
||
'# history[0] -- последний шаг (самый свежий)',
|
||
'# history[-1] -- самый первый (начало сессии)',
|
||
'',
|
||
'for i, snap in enumerate(history):',
|
||
' print(i, snap.metadata.get("step"), snap.values.get("iter"))',
|
||
'',
|
||
'# replays = форк от любого прошлого snapshot:',
|
||
'old = history[2].config',
|
||
'app.invoke(None, old) # переигрывает только следующие шаги',
|
||
].join('\n'),
|
||
highlightLines: [2, 3, 4, 7, 10, 11],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/persistence/#time-travel',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 18 -- update_state: fork and replay
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'TIME TRAVEL / 2',
|
||
title: 'update_state: форкнуть состояние и пойти другой веткой',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'# patch значения канала прямо в snapshot -- создаётся новый checkpoint',
|
||
'app.update_state(',
|
||
' cfg,',
|
||
' values={"messages": [{"role": "user", "content": "rewind"}]},',
|
||
' as_node="user_input", # от имени какого узла пишем',
|
||
')',
|
||
'',
|
||
'# Дальше invoke(None, cfg) переигрывает граф с нового состояния',
|
||
'app.invoke(None, cfg)',
|
||
'',
|
||
'# Типичный приём: "что если пользователь сказал не X, а Y?" --',
|
||
'# ответвляемся, смотрим альтернативный прогон без потери истории.',
|
||
].join('\n'),
|
||
highlightLines: [2, 3, 4, 5, 10, 11],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/persistence/#update-state',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 19 -- HITL: interrupt + Command(resume)
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'HITL / 1',
|
||
title: 'interrupt + Command(resume=): пауза на человеке',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'from langgraph.types import interrupt, Command',
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'from langgraph.checkpoint.memory import InMemorySaver',
|
||
'from typing_extensions import TypedDict',
|
||
'class State(TypedDict):',
|
||
' question: str',
|
||
' approved: bool',
|
||
'def ask(state: State):',
|
||
' # interrupt() -- пауза. Возвращает значение из Command(resume=...)',
|
||
' answer = interrupt({"question": "Approve sending this message?"})',
|
||
' return {"approved": answer == "yes"}',
|
||
'g = StateGraph(State)',
|
||
'g.add_node("ask", ask)',
|
||
'g.add_edge(START, "ask")',
|
||
'g.add_edge("ask", END)',
|
||
'app = g.compile(checkpointer=InMemorySaver())',
|
||
'cfg = {"configurable": {"thread_id": "approval-1"}}',
|
||
'app.invoke({"question": "send email", "approved": False}, cfg) # пауза',
|
||
'result = app.invoke(Command(resume="yes"), cfg) # resume',
|
||
'print(result["approved"]) # -> True',
|
||
].join('\n'),
|
||
highlightLines: [10, 17, 18, 19],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/human_in_the_loop/',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 20 -- HITL: graph.invoke with config + pause
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'HITL / 2',
|
||
title: 'Как выглядит HITL-цикл снаружи',
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 4.5, h: 3.5,
|
||
kind: 'info',
|
||
title: 'Серверная сторона',
|
||
text: '1. graph.invoke(input, cfg)\n' +
|
||
'2. Внутри узла вызван interrupt(payload)\n' +
|
||
'3. Граф замораживается, состояние в checkpointer\n' +
|
||
'4. Возвращается GraphInterrupt с payload\n' +
|
||
'5. Сервер ждёт -- пользователь думает часы/дни',
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 5.2, y: layouts.CONTENT_TOP, w: 4.3, h: 3.5,
|
||
kind: 'success',
|
||
title: 'Клиентская сторона',
|
||
text: '1. UI получает payload, рисует форму\n' +
|
||
'2. Пользователь жмёт Approve / Reject\n' +
|
||
'3. UI делает POST /threads/{id}/resume\n' +
|
||
'4. Сервер вызывает graph.invoke(Command(resume=answer), cfg)\n' +
|
||
'5. Граф оживает с того же узла',
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'blog.langchain.com/langchain-langgraph-1dot0 (HITL section)',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 21 -- HITL: multi-turn approval cycle (review-node only)
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'HITL / 3',
|
||
title: 'Multi-turn approval: узел review',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'# Узел review -- маршрутизация по ответу человека',
|
||
'from langgraph.types import interrupt, Command',
|
||
'from typing_extensions import TypedDict',
|
||
'class State(TypedDict):',
|
||
' plan: str',
|
||
' executed: bool',
|
||
'def plan(state: State):',
|
||
' return {"plan": "step-A; step-B; step-C"}',
|
||
'def review(state: State) -> Command:',
|
||
' # interrupt() возвращает ответ из Command(resume=...)',
|
||
' decision = interrupt({"plan": state["plan"]})',
|
||
' if decision == "approve":',
|
||
' return Command(goto="execute")',
|
||
' if decision == "abort":',
|
||
' return Command(goto=END)',
|
||
' return Command(goto="plan") # перепланировать',
|
||
'def execute(state: State):',
|
||
' return {"executed": True}',
|
||
].join('\n'),
|
||
highlightLines: [9, 10, 11, 12, 13, 14, 15],
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 0.5, y: 4.0, w: 9.0, h: 0.65,
|
||
kind: 'info',
|
||
text: 'Полный пример со сборкой графа -- на следующем слайде.',
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/cycle/',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 21b -- HITL: full graph assembly for multi-turn approval
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'HITL / 4',
|
||
title: 'Multi-turn approval: сборка графа',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'from langgraph.checkpoint.memory import InMemorySaver',
|
||
'# plan, review, execute -- из предыдущего слайда',
|
||
'g = StateGraph(State)',
|
||
'g.add_node("plan", plan)',
|
||
'g.add_node("review", review)',
|
||
'g.add_node("execute", execute)',
|
||
'g.add_edge(START, "plan")',
|
||
'g.add_edge("plan", "review")',
|
||
'g.add_edge("execute", END)',
|
||
'app = g.compile(checkpointer=InMemorySaver())',
|
||
'',
|
||
'# Цикл: каждый review -- это пауза; Command(goto=...) -- ответвление',
|
||
'# plan -> review -> (approve: execute | abort: END | else: plan)',
|
||
'cfg = {"configurable": {"thread_id": "u1"}}',
|
||
'app.invoke({"plan": "", "executed": False}, cfg) # пауза 1',
|
||
'app.invoke(Command(resume="approve"), cfg) # resume -> execute',
|
||
].join('\n'),
|
||
highlightLines: [5, 6, 8, 14, 15],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/cycle/',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 22 -- Subgraphs
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'SUBGRAPHS',
|
||
title: 'Subgraphs: композитность и изоляция state',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'from typing_extensions import TypedDict',
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'',
|
||
'class SubState(TypedDict):',
|
||
' internal: str',
|
||
'',
|
||
'def inner(state: SubState):',
|
||
' return {"internal": "sub-output"}',
|
||
'',
|
||
'# Собираем subgraph -- у него свой state',
|
||
'sub = StateGraph(SubState)',
|
||
'sub.add_node("inner", inner)',
|
||
'sub.add_edge(START, "inner")',
|
||
'sub.add_edge("inner", END)',
|
||
'sub_compiled = sub.compile()',
|
||
'',
|
||
'# Вставляем как обычный узел в родительский граф',
|
||
'class ParentState(TypedDict):',
|
||
' out: str',
|
||
'',
|
||
'parent = StateGraph(ParentState)',
|
||
'parent.add_node("sub_block", sub_compiled) # <- subgraph целиком',
|
||
'parent.add_edge(START, "sub_block")',
|
||
'parent.add_edge("sub_block", END)',
|
||
'app = parent.compile()',
|
||
'print(app.invoke({"out": ""}))',
|
||
].join('\n'),
|
||
highlightLines: [11, 12, 13, 14, 21, 22],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/subgraphs/',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 23 -- Streaming: 5 modes
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'STREAMING / 1',
|
||
title: 'Пять режимов stream_mode',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'cfg = {"configurable": {"thread_id": "u1"}}',
|
||
'# values -- весь state после каждого super-step',
|
||
'for snap in app.stream({"messages": []}, cfg, stream_mode="values"):',
|
||
' print(snap["messages"][-1].content)',
|
||
'# updates -- delta: только что вернул каждый узел',
|
||
'for upd in app.stream({"messages": []}, cfg, stream_mode="updates"):',
|
||
' print(upd)',
|
||
'# events -- низкоуровневые события (start, end, error, interrupt)',
|
||
'for ev in app.stream({"messages": []}, cfg, stream_mode="events"):',
|
||
' print(ev["event"], ev["name"])',
|
||
'# messages -- токены LLM по мере генерации',
|
||
'for tok, meta in app.stream({"messages": []}, cfg, stream_mode="messages"):',
|
||
' print(tok.content, end="|")',
|
||
'# custom -- только то, что узлы пишут через get_stream_writer()',
|
||
'for chunk in app.stream({"messages": []}, cfg, stream_mode="custom"):',
|
||
' print(chunk)',
|
||
].join('\n'),
|
||
highlightLines: [3, 6, 9, 12, 15],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/streaming/',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 24 -- Custom streaming with get_stream_writer
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'STREAMING / 2',
|
||
title: 'Custom stream: пишем из узла как хотим',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'from langgraph.config import get_stream_writer',
|
||
'from typing_extensions import TypedDict',
|
||
'',
|
||
'class State(TypedDict):',
|
||
' total: int',
|
||
'',
|
||
'def progress(state: State):',
|
||
' writer = get_stream_writer() # доступен только во время выполнения узла',
|
||
' for i in range(3):',
|
||
' writer({"progress": i, "phase": "thinking"})',
|
||
' return {"total": 3}',
|
||
'',
|
||
'g = StateGraph(State)',
|
||
'g.add_node("progress", progress)',
|
||
'g.add_edge(START, "progress")',
|
||
'g.add_edge("progress", END)',
|
||
'app = g.compile()',
|
||
'',
|
||
'# в UI -- только custom-чанки, без промежуточного state',
|
||
'for chunk in app.stream({"total": 0}, stream_mode="custom"):',
|
||
' print(chunk)',
|
||
].join('\n'),
|
||
highlightLines: [10, 11, 12, 19, 20],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/streaming/#custom',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 25 -- Tool calling: tools + LLM binding
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'TOOL CALLING / 1',
|
||
title: 'Tool calling: tools + LLM binding',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'from langchain_openai import ChatOpenAI',
|
||
'from langchain.tools import tool',
|
||
'from langgraph.graph.message import add_messages',
|
||
'from typing import Annotated',
|
||
'from typing_extensions import TypedDict',
|
||
'@tool',
|
||
'def add(a: int, b: int) -> int:',
|
||
' "Add two numbers."',
|
||
' return a + b',
|
||
'tools = [add]',
|
||
'llm = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools)',
|
||
'class State(TypedDict):',
|
||
' messages: Annotated[list, add_messages]',
|
||
'def agent(state: State):',
|
||
' return {"messages": [llm.invoke(state["messages"])]}',
|
||
'def route(state: State) -> str:',
|
||
' last = state["messages"][-1]',
|
||
' return "tools" if getattr(last, "tool_calls", None) else END',
|
||
].join('\n'),
|
||
highlightLines: [11, 17],
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 0.5, y: 4.0, w: 9.0, h: 0.65,
|
||
kind: 'info',
|
||
text: 'Сборка графа и запуск -- на следующем слайде.',
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/how-tos/tool-calling/',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 25b -- Tool calling: graph assembly + run
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'TOOL CALLING / 2',
|
||
title: 'Tool calling: сборка графа и запуск',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'from langgraph.graph import StateGraph, START, END',
|
||
'from langgraph.prebuilt import ToolNode',
|
||
'# tools, llm, agent, route -- из предыдущего слайда',
|
||
'g = StateGraph(State)',
|
||
'g.add_node("agent", agent)',
|
||
'g.add_node("tools", ToolNode(tools))',
|
||
'g.add_edge(START, "agent")',
|
||
'g.add_conditional_edges("agent", route, {"tools": "tools", END: END})',
|
||
'g.add_edge("tools", "agent")',
|
||
'app = g.compile()',
|
||
'',
|
||
'# Цикл: agent решает -> tools исполняет -> agent снова читает результат',
|
||
'result = app.invoke({',
|
||
' "messages": [{"role": "user", "content": "What is 2 + 3?"}]',
|
||
'})',
|
||
'print(result["messages"][-1].content)',
|
||
].join('\n'),
|
||
highlightLines: [6, 10, 13, 14],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/how-tos/tool-calling/',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 26 -- LangGraph Studio
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'STUDIO',
|
||
title: 'LangGraph Studio: визуальный debugger',
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 4.5, h: 3.0,
|
||
kind: 'info',
|
||
title: 'Что это',
|
||
text: 'Desktop IDE и web-UI для отладки графов. ' +
|
||
'Показывает граф, каждый super-step, ' +
|
||
'state на каждом шаге, время на узел.',
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 5.2, y: layouts.CONTENT_TOP, w: 4.3, h: 3.0,
|
||
kind: 'success',
|
||
title: 'Что умеет',
|
||
text: '- запускать граф интерактивно\n' +
|
||
'- ставить breakpoints на узлах\n' +
|
||
'- модифицировать state вручную\n' +
|
||
'- редактировать узлы и перезапускать\n' +
|
||
'- экспорт trace в LangSmith',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: 4.2, w: 9.0, h: 0.8,
|
||
code: [
|
||
'# запуск: langgraph dev -- поднимает Studio на http://localhost:8123',
|
||
].join('\n'),
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/langgraph_studio/',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 27 -- Deploy через LangGraph Platform
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'PLATFORM',
|
||
title: 'Deploy через LangGraph Platform',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 1.3,
|
||
code: [
|
||
'# langgraph.json -- declarative config',
|
||
'{',
|
||
' "graphs": {"agent": "./agent.py:graph"},',
|
||
' "env": "./.env",',
|
||
' "python_version": "3.11"',
|
||
'}',
|
||
].join('\n'),
|
||
highlightLines: [2],
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: 2.8, w: 9.0, h: 2.0,
|
||
code: [
|
||
'# CLI: локальный dev-сервер и deploy',
|
||
'langgraph dev # локальный API + Studio',
|
||
'langgraph up # Docker-compose stack (Redis + API + Studio)',
|
||
'langgraph deploy # пуш в LangGraph Platform (managed)',
|
||
].join('\n'),
|
||
highlightLines: [3],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'langchain-ai.github.io/langgraph/concepts/langgraph_platform/ (managed: scaling, queue, persistent threads, observability)',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 28 -- What's new in 1.0
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'NEW IN 1.0',
|
||
title: 'Что нового в LangGraph 1.0: четыре фичи',
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 4.5, h: 1.55,
|
||
kind: 'success',
|
||
title: 'Durable execution',
|
||
text: 'Состояние персистится автоматически. Падение сервера посреди long-run -- ' +
|
||
'граф восстанавливается ровно с точки остановки.',
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 5.2, y: layouts.CONTENT_TOP, w: 4.3, h: 1.55,
|
||
kind: 'success',
|
||
title: 'HITL first-class',
|
||
text: 'interrupt() -- стабильный API, multi-day approval workflows без костылей.',
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 0.5, y: 3.1, w: 4.5, h: 1.5,
|
||
kind: 'info',
|
||
title: 'Built-in persistence',
|
||
text: 'InMemorySaver / SqliteSaver / PostgresSaver -- first-class контракты, ' +
|
||
'а не отдельный набор фич.',
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 5.2, y: 3.1, w: 4.3, h: 1.5,
|
||
kind: 'warning',
|
||
title: 'Breaking changes',
|
||
text: '- langgraph.prebuilt.create_react_agent -> langchain.agents.create_agent\n' +
|
||
'- MemorySaver -> InMemorySaver (новый alias)\n' +
|
||
'- semver: стабильно до 2.0',
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'changelog.langchain.com/announcements/langgraph-1-0-is-now-generally-available (bonus 1.2: fault tolerance middleware -- retries / timeouts / error handlers)',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 29 -- TypeScript analogue
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'TYPESCRIPT',
|
||
title: '@langchain/langgraph -- JS/TS-аналог',
|
||
});
|
||
|
||
addCodeBlock(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65,
|
||
code: [
|
||
'import { StateGraph, START, END, Annotation } from "@langchain/langgraph";',
|
||
'import { MemorySaver } from "@langchain/langgraph-checkpoint";',
|
||
'',
|
||
'const State = Annotation.Root({',
|
||
' messages: Annotation({',
|
||
' reducer: (a, b) => a.concat(b),',
|
||
' default: () => [],',
|
||
' }),',
|
||
'});',
|
||
'',
|
||
'const g = new StateGraph(State)',
|
||
' .addNode("echo", (s) => ({',
|
||
' messages: [{ role: "assistant", content: "echo: " + s.messages.at(-1).content }],',
|
||
' }))',
|
||
' .addEdge(START, "echo")',
|
||
' .addEdge("echo", END);',
|
||
'',
|
||
'const app = g.compile({ checkpointer: new MemorySaver() });',
|
||
'const cfg = { configurable: { thread_id: "t1" } };',
|
||
'const result = await app.invoke({ messages: [{ role: "user", content: "hi" }] }, cfg);',
|
||
].join('\n'),
|
||
highlightLines: [1, 2, 19, 21],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'github.com/langchain-ai/langgraphjs',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 30 -- pros/cons: LangGraph vs LCEL
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'WHEN TO USE',
|
||
title: 'Когда LangGraph, когда остаться на LCEL',
|
||
});
|
||
|
||
addProsCons(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.5,
|
||
pros: [
|
||
'Агент крутится в цикле (tool calls + retry)',
|
||
'Нужна пауза на human approval / review',
|
||
'Long-running workflow переживает рестарт',
|
||
'Сложная топология: ветвления, merge, map-reduce',
|
||
'Multi-agent: subgraphs + Send',
|
||
'Time-travel и replay для отладки',
|
||
],
|
||
cons: [
|
||
'Простой pipeline prompt | model | parser',
|
||
'Один проход без state между вызовами',
|
||
'Read-only чат без persistence',
|
||
'Быстрый прототип без долгоживущего state',
|
||
'Команда не готова к concepts: channels/reducers/Send',
|
||
],
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'blog.langchain.com/langchain-langgraph-1dot0',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Slide 31 -- Bridge to Deep Agents
|
||
// ---------------------------------------------------------------------------
|
||
{
|
||
const s = pres.addSlide();
|
||
slideBase(s, pres, theme);
|
||
addHeader(s, pres, theme, {
|
||
section: SECTION_LABEL,
|
||
sectionNumber: SECTION_NUMBER,
|
||
eyebrow: 'BRIDGE TO SECTION 3',
|
||
title: 'Мостик к Deep Agents',
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 0.5, y: layouts.CONTENT_TOP, w: 4.5, h: 1.8,
|
||
kind: 'info',
|
||
title: 'Что мы только что разобрали',
|
||
text: '- State, nodes, edges, persistence\n' +
|
||
'- HITL через interrupt + Command(resume)\n' +
|
||
'- Subgraphs, streaming, ToolNode\n' +
|
||
'- Deploy через LangGraph Platform',
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 5.2, y: layouts.CONTENT_TOP, w: 4.3, h: 1.8,
|
||
kind: 'success',
|
||
title: 'Что дальше (Section 3)',
|
||
text: 'Deep Agents -- это готовая архитектура поверх LangGraph: ' +
|
||
'planning tool, filesystem, subagents, middleware. ' +
|
||
'create_deep_agent -- одна функция вместо сотни строк boilerplate.',
|
||
});
|
||
|
||
addCallout(s, pres, theme, {
|
||
x: 0.5, y: 3.4, w: 9.0, h: 1.3,
|
||
kind: 'warning',
|
||
title: 'Связь',
|
||
text: 'create_deep_agent из deepagents==0.6.11 -- это обёртка, которая собирает граф LangGraph ' +
|
||
'с planning tool, файловой системой и subagents. Внутри всё, что мы видели: ' +
|
||
'StateGraph, Command, interrupt, subgraphs.',
|
||
});
|
||
|
||
addSourceLine(s, pres, theme, {
|
||
source: 'github.com/langchain-ai/deepagents (README)',
|
||
});
|
||
addPageNumber(s, pres, theme, next());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Build
|
||
// ---------------------------------------------------------------------------
|
||
const outFile = path.join(__dirname, 'section2.pptx');
|
||
pres.writeFile({ fileName: outFile }).then(function (file) {
|
||
console.log('Wrote: ' + file);
|
||
}).catch(function (err) {
|
||
console.error('ERROR:', err);
|
||
process.exit(1);
|
||
});
|