Files
langchain-evolution-deck/slides/section2-langgraph/compile.js
T
petya 75601988c2 Initial commit: LangChain evolution tutorial deck (132 slides)
- 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
2026-06-22 11:29:03 +03:00

1530 lines
54 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});